python-openevse-http 1.3.0__tar.gz → 1.5.0__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 (76) hide show
  1. python_openevse_http-1.5.0/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
  2. python_openevse_http-1.5.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
  3. python_openevse_http-1.5.0/.github/ISSUE_TEMPLATE/feature_request.yml +25 -0
  4. python_openevse_http-1.5.0/.github/pull_request_template.md +26 -0
  5. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/.github/workflows/autolabeler.yml +1 -1
  6. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/.github/workflows/links.yml +1 -1
  7. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/.github/workflows/publish-to-pypi.yml +1 -1
  8. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/.github/workflows/release-drafter.yml +1 -1
  9. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/.github/workflows/test.yml +3 -3
  10. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/PKG-INFO +30 -1
  11. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/README.md +29 -0
  12. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/example_external_session.py +18 -0
  13. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/openevsehttp/client.py +9 -1
  14. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/openevsehttp/commands.py +22 -0
  15. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/openevsehttp/properties.py +23 -0
  16. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/openevsehttp/websocket.py +10 -2
  17. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/pyproject.toml +1 -1
  18. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/python_openevse_http.egg-info/PKG-INFO +30 -1
  19. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/python_openevse_http.egg-info/SOURCES.txt +6 -0
  20. python_openevse_http-1.5.0/python_openevse_http.egg-info/scm_file_list.json +70 -0
  21. python_openevse_http-1.5.0/python_openevse_http.egg-info/scm_version.json +8 -0
  22. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/requirements_lint.txt +2 -2
  23. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/requirements_test.txt +2 -2
  24. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/test_client.py +41 -0
  25. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/test_commands.py +33 -0
  26. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/test_properties.py +27 -1
  27. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/test_websocket.py +52 -0
  28. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/.github/dependabot.yml +0 -0
  29. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/.github/release-drafter.yml +0 -0
  30. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/.gitignore +0 -0
  31. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/.pre-commit-config.yaml +0 -0
  32. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/.yamllint +0 -0
  33. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/EXTERNAL_SESSION.md +0 -0
  34. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/LICENSE +0 -0
  35. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/codecov.yml +0 -0
  36. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/openevsehttp/__init__.py +0 -0
  37. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/openevsehttp/__main__.py +0 -0
  38. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/openevsehttp/const.py +0 -0
  39. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/openevsehttp/exceptions.py +0 -0
  40. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/openevsehttp/managers.py +0 -0
  41. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/openevsehttp/py.typed +0 -0
  42. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/openevsehttp/sensors.py +0 -0
  43. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/openevsehttp/utils.py +0 -0
  44. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/python_openevse_http.egg-info/dependency_links.txt +0 -0
  45. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/python_openevse_http.egg-info/not-zip-safe +0 -0
  46. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/python_openevse_http.egg-info/requires.txt +0 -0
  47. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/python_openevse_http.egg-info/top_level.txt +0 -0
  48. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/requirements.txt +0 -0
  49. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/setup.cfg +0 -0
  50. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/setup.py +0 -0
  51. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/__init__.py +0 -0
  52. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/common.py +0 -0
  53. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/conftest.py +0 -0
  54. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/fixtures/github_v2.json +0 -0
  55. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/fixtures/github_v4.json +0 -0
  56. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/fixtures/v2_json/config.json +0 -0
  57. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/fixtures/v2_json/status.json +0 -0
  58. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/fixtures/v4_json/config-broken-semver.json +0 -0
  59. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/fixtures/v4_json/config-broken.json +0 -0
  60. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/fixtures/v4_json/config-dev.json +0 -0
  61. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/fixtures/v4_json/config-extra-version.json +0 -0
  62. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/fixtures/v4_json/config-new.json +0 -0
  63. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/fixtures/v4_json/config-unknown-semver.json +0 -0
  64. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/fixtures/v4_json/config.json +0 -0
  65. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/fixtures/v4_json/schedule.json +0 -0
  66. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/fixtures/v4_json/status-broken.json +0 -0
  67. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/fixtures/v4_json/status-new.json +0 -0
  68. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/fixtures/v4_json/status.json +0 -0
  69. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/fixtures/websocket.json +0 -0
  70. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/test_external_session.py +0 -0
  71. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/test_main_edge_cases.py +0 -0
  72. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/test_managers.py +0 -0
  73. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/test_mixins.py +0 -0
  74. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/test_sensors.py +0 -0
  75. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tests/test_shaper.py +0 -0
  76. {python_openevse_http-1.3.0 → python_openevse_http-1.5.0}/tox.ini +0 -0
@@ -0,0 +1,62 @@
1
+ name: 🐛 Bug Report
2
+ labels: ["bug"]
3
+ title: "[Bug]: "
4
+ description: Report a bug in python-openevse-http.
5
+ body:
6
+ - type: textarea
7
+ validations:
8
+ required: true
9
+ attributes:
10
+ label: Describe the bug
11
+ description: A clear and concise description of what the bug is. Tell us what you were trying to do and what happened.
12
+ placeholder: E.g., calling OpenEVSE.update() raises an exception.
13
+ - type: textarea
14
+ validations:
15
+ required: true
16
+ attributes:
17
+ label: Steps to reproduce
18
+ description: Steps to reproduce the behavior.
19
+ placeholder: |
20
+ 1. Initialize OpenEVSE with ...
21
+ 2. Call ...
22
+ 3. See error
23
+ - type: textarea
24
+ validations:
25
+ required: true
26
+ attributes:
27
+ label: Expected behavior
28
+ description: A clear and concise description of what you expected to happen.
29
+ - type: markdown
30
+ attributes:
31
+ value: |
32
+ ## Environment Info
33
+ - type: input
34
+ id: version
35
+ validations:
36
+ required: true
37
+ attributes:
38
+ label: Library version
39
+ placeholder: e.g., 1.0.0, main branch
40
+ - type: input
41
+ id: python_version
42
+ validations:
43
+ required: true
44
+ attributes:
45
+ label: Python version
46
+ placeholder: e.g., 3.10, 3.11
47
+ - type: input
48
+ id: firmware_version
49
+ validations:
50
+ required: false
51
+ attributes:
52
+ label: OpenEVSE WiFi Firmware version
53
+ placeholder: e.g., 4.1.2, 5.0.0
54
+ - type: textarea
55
+ attributes:
56
+ label: Debug Logs / Stack Trace
57
+ description: Please paste any relevant traceback outputs or logs here (enable debug logging if possible).
58
+ render: txt
59
+ - type: textarea
60
+ attributes:
61
+ label: Additional context
62
+ description: Add any other context or screenshots about the bug here.
@@ -0,0 +1,5 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Ask a question / Support Request
4
+ url: https://github.com/firstof9/python-openevse-http/discussions
5
+ about: Please ask questions and get support on GitHub Discussions.
@@ -0,0 +1,25 @@
1
+ name: ✨ Feature Request
2
+ labels: ["feature"]
3
+ title: "[Feature Request]: "
4
+ description: Request a feature for python-openevse-http.
5
+ body:
6
+ - type: textarea
7
+ validations:
8
+ required: true
9
+ attributes:
10
+ label: Is your feature request related to a problem? Please describe.
11
+ description: A clear and concise description of what the problem is. E.g. I'm always frustrated when [...]
12
+ - type: textarea
13
+ validations:
14
+ required: true
15
+ attributes:
16
+ label: Describe the solution you'd like
17
+ description: A clear and concise description of what you want to happen.
18
+ - type: textarea
19
+ attributes:
20
+ label: Describe alternatives you've considered
21
+ description: A clear and concise description of any alternative solutions or features you've considered.
22
+ - type: textarea
23
+ attributes:
24
+ label: Additional context
25
+ description: Add any other context or screenshots about the feature request here.
@@ -0,0 +1,26 @@
1
+ ## Description
2
+
3
+ Please include a summary of the changes and the related issue. Please also include relevant motivation and context.
4
+
5
+ Fixes # (issue)
6
+
7
+ ## Type of change
8
+
9
+ Please delete options that are not relevant.
10
+
11
+ - [ ] Bug fix (non-breaking change which fixes an issue)
12
+ - [ ] New feature (non-breaking change which adds functionality)
13
+ - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
14
+ - [ ] Code quality / Refactoring
15
+ - [ ] Documentation update
16
+
17
+ ## Checklist
18
+
19
+ - [ ] My code follows the style guidelines of this project
20
+ - [ ] I have performed a self-review of my own code
21
+ - [ ] I have commented my code, particularly in hard-to-understand areas
22
+ - [ ] I have made corresponding changes to the documentation
23
+ - [ ] My changes generate no new warnings
24
+ - [ ] I have added tests that prove my fix is effective or that my feature works
25
+ - [ ] New and existing unit tests pass locally with my changes
26
+ - [ ] Any dependent changes have been merged and published in downstream modules
@@ -38,6 +38,6 @@ jobs:
38
38
  with:
39
39
  egress-policy: audit
40
40
 
41
- - uses: release-drafter/release-drafter/autolabeler@693d20e7c1ce1a81d3a41962f85914253b518449 # v7
41
+ - uses: release-drafter/release-drafter/autolabeler@ed4bc48ec97379be2258e7b7ac2624a3e26ab809 # v7
42
42
  env:
43
43
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -19,7 +19,7 @@ jobs:
19
19
  with:
20
20
  egress-policy: audit
21
21
 
22
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
22
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
23
23
 
24
24
  - name: Link Checker
25
25
  uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2
@@ -25,7 +25,7 @@ jobs:
25
25
  with:
26
26
  egress-policy: audit
27
27
 
28
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
28
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
29
29
  with:
30
30
  ref: ${{ inputs.tag || github.ref }}
31
31
  fetch-depth: 0
@@ -18,6 +18,6 @@ jobs:
18
18
  egress-policy: audit
19
19
 
20
20
  # Drafts your next Release notes as Pull Requests are merged into "main"
21
- - uses: release-drafter/release-drafter@693d20e7c1ce1a81d3a41962f85914253b518449 # v7
21
+ - uses: release-drafter/release-drafter@ed4bc48ec97379be2258e7b7ac2624a3e26ab809 # v7
22
22
  env:
23
23
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -22,7 +22,7 @@ jobs:
22
22
  with:
23
23
  egress-policy: audit
24
24
 
25
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
25
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
26
26
  - name: Set up Python
27
27
  uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
28
28
  with:
@@ -45,7 +45,7 @@ jobs:
45
45
  with:
46
46
  egress-policy: audit
47
47
 
48
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
48
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
49
49
  with:
50
50
  fetch-depth: 2
51
51
  - name: Set up Python ${{ matrix.python-version }}
@@ -74,7 +74,7 @@ jobs:
74
74
  egress-policy: audit
75
75
 
76
76
  - name: Check out the repository
77
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
77
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
78
78
  with:
79
79
  fetch-depth: 2
80
80
  - name: Download coverage data
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_openevse_http
3
- Version: 1.3.0
3
+ Version: 1.5.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
@@ -84,6 +84,35 @@ async def main():
84
84
  if __name__ == "__main__":
85
85
  asyncio.run(main())
86
86
  ```
87
+ ### HTTPS and SSL Verification Options
88
+
89
+ If your OpenEVSE WiFi/ethernet module uses HTTPS, you can configure the client to connect securely using the `ssl=True` parameter:
90
+
91
+ ```python
92
+ # Connect securely using HTTPS (validating SSL/TLS certificates)
93
+ charger = OpenEVSE(
94
+ "192.168.1.30",
95
+ session=session,
96
+ ssl=True,
97
+ )
98
+ ```
99
+
100
+ #### Bypassing SSL Verification (Self-Signed Certificates)
101
+
102
+ > [!WARNING]
103
+ > Disabling SSL certificate validation (`ssl_verify=False`) disables TLS verification and exposes the connection to Man-in-the-Middle (MITM) attacks. Only use this configuration when connecting to an OpenEVSE module with a self-signed certificate over a trusted local network.
104
+
105
+ To bypass certificate verification:
106
+
107
+ ```python
108
+ # Connect using HTTPS, and disable certificate verification
109
+ charger = OpenEVSE(
110
+ "192.168.1.30",
111
+ session=session,
112
+ ssl=True,
113
+ ssl_verify=False,
114
+ )
115
+ ```
87
116
 
88
117
  ## API Support Matrix
89
118
 
@@ -51,6 +51,35 @@ async def main():
51
51
  if __name__ == "__main__":
52
52
  asyncio.run(main())
53
53
  ```
54
+ ### HTTPS and SSL Verification Options
55
+
56
+ If your OpenEVSE WiFi/ethernet module uses HTTPS, you can configure the client to connect securely using the `ssl=True` parameter:
57
+
58
+ ```python
59
+ # Connect securely using HTTPS (validating SSL/TLS certificates)
60
+ charger = OpenEVSE(
61
+ "192.168.1.30",
62
+ session=session,
63
+ ssl=True,
64
+ )
65
+ ```
66
+
67
+ #### Bypassing SSL Verification (Self-Signed Certificates)
68
+
69
+ > [!WARNING]
70
+ > Disabling SSL certificate validation (`ssl_verify=False`) disables TLS verification and exposes the connection to Man-in-the-Middle (MITM) attacks. Only use this configuration when connecting to an OpenEVSE module with a self-signed certificate over a trusted local network.
71
+
72
+ To bypass certificate verification:
73
+
74
+ ```python
75
+ # Connect using HTTPS, and disable certificate verification
76
+ charger = OpenEVSE(
77
+ "192.168.1.30",
78
+ session=session,
79
+ ssl=True,
80
+ ssl_verify=False,
81
+ )
82
+ ```
54
83
 
55
84
  ## API Support Matrix
56
85
 
@@ -49,6 +49,24 @@ async def example_shared_session():
49
49
  await charger2.ws_disconnect()
50
50
 
51
51
 
52
+ async def example_with_https_and_ssl_bypass():
53
+ """Demonstrate using HTTPS with bypassed SSL verification."""
54
+ async with aiohttp.ClientSession() as session:
55
+ # Pass ssl=True to construct an https:// URL,
56
+ # and ssl_verify=False to bypass certificate validation.
57
+ charger = OpenEVSE(
58
+ "192.168.1.30",
59
+ session=session,
60
+ ssl=True,
61
+ ssl_verify=False,
62
+ )
63
+
64
+ # Use the charger normally
65
+ await charger.update()
66
+ print(f"Status: {charger.status}")
67
+ await charger.ws_disconnect()
68
+
69
+
52
70
  if __name__ == "__main__":
53
71
  # Run one of the examples
54
72
  asyncio.run(example_with_external_session())
@@ -53,11 +53,16 @@ class OpenEVSE(CommandsMixin, ManagersMixin, SensorsMixin, PropertiesMixin):
53
53
  user: str | None = None,
54
54
  pwd: str | None = None,
55
55
  session: aiohttp.ClientSession | None = None,
56
+ ssl: bool = False,
57
+ ssl_verify: bool = True,
56
58
  ) -> None:
57
59
  """Connect to an OpenEVSE charger equipped with wifi or ethernet."""
58
60
  self._user = user or ""
59
61
  self._pwd = pwd or ""
60
- self.url = f"http://{host}/"
62
+ self.ssl = ssl
63
+ self.ssl_verify = ssl_verify
64
+ scheme = "https" if ssl else "http"
65
+ self.url = f"{scheme}://{host}/"
61
66
  self._status: dict[str, Any] = {}
62
67
  self._config: dict[str, Any] = {}
63
68
  self._override: Any = None
@@ -134,6 +139,8 @@ class OpenEVSE(CommandsMixin, ManagersMixin, SensorsMixin, PropertiesMixin):
134
139
  kwargs = {"data": rapi, "auth": auth}
135
140
  if data is not None:
136
141
  kwargs["json"] = data
142
+ if url.startswith("https://") and not self.ssl_verify:
143
+ kwargs["ssl"] = False
137
144
  async with http_method(url, **kwargs) as resp:
138
145
  try:
139
146
  raw = await resp.text()
@@ -285,6 +292,7 @@ class OpenEVSE(CommandsMixin, ManagersMixin, SensorsMixin, PropertiesMixin):
285
292
  self._user,
286
293
  self._pwd,
287
294
  self._session,
295
+ ssl_verify=self.ssl_verify,
288
296
  )
289
297
 
290
298
  def _validate_session_loop(self, loop: asyncio.AbstractEventLoop) -> None:
@@ -23,6 +23,8 @@ class CommandsMixin:
23
23
  """Mixin providing command methods for OpenEVSE."""
24
24
 
25
25
  url: str
26
+ ssl: bool
27
+ ssl_verify: bool
26
28
  _status: dict[str, Any]
27
29
  _config: dict[str, Any]
28
30
  _session: aiohttp.ClientSession | None
@@ -665,3 +667,23 @@ class CommandsMixin:
665
667
 
666
668
  new_state = not bool(shaper_active)
667
669
  await self.set_shaper(new_state)
670
+
671
+ async def set_mqtt_vehicle_range_miles(self, enable: bool = True) -> None:
672
+ """Set mqtt_vehicle_range_miles configuration setting.
673
+
674
+ Dynamically changing this setting will affect future evaluations of
675
+ the vehicle_range_with_unit property.
676
+ """
677
+ if not isinstance(enable, bool):
678
+ raise TypeError("Value must be a boolean.")
679
+
680
+ url = f"{self.url}config"
681
+ data = {"mqtt_vehicle_range_miles": enable}
682
+
683
+ _LOGGER.debug("Setting mqtt_vehicle_range_miles to %s", enable)
684
+ response = await self.process_request(url=url, method="post", data=data)
685
+ response = self._normalize_response(response)
686
+ msg = response.get("msg") if isinstance(response, Mapping) else None
687
+ if msg not in SUCCESS_ANSWERS:
688
+ _LOGGER.error("Problem issuing command: %s", response)
689
+ raise UnknownError
@@ -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."""
@@ -42,9 +42,11 @@ class OpenEVSEWebsocket:
42
42
  user: str | None = None,
43
43
  password: str | None = None,
44
44
  session: aiohttp.ClientSession | None = None,
45
+ ssl_verify: bool = True,
45
46
  ) -> None:
46
47
  """Initialize a OpenEVSEWebsocket instance."""
47
48
  self.session = session
49
+ self.ssl_verify = ssl_verify
48
50
  self.uri = self._get_uri(server)
49
51
  self._user = user
50
52
  self._password = password
@@ -133,10 +135,16 @@ class OpenEVSEWebsocket:
133
135
  try:
134
136
  # Narrow type for mypy since _ensure_session sets self.session
135
137
  assert self.session is not None
138
+ ws_kwargs: dict[str, Any] = {
139
+ "heartbeat": 15,
140
+ "auth": auth,
141
+ }
142
+ if self.uri.startswith("wss://") and not self.ssl_verify:
143
+ ws_kwargs["ssl"] = False
144
+
136
145
  async with self.session.ws_connect(
137
146
  self.uri,
138
- heartbeat=15,
139
- auth=auth,
147
+ **ws_kwargs,
140
148
  ) as ws_client:
141
149
  self._client = ws_client
142
150
  await self._set_state(STATE_CONNECTED)
@@ -32,7 +32,7 @@ ignore = [
32
32
  max-complexity = 18
33
33
 
34
34
  [build-system]
35
- requires = ["setuptools>=82.0.1", "setuptools-scm>=10.0.5"]
35
+ requires = ["setuptools>=82.0.1", "setuptools-scm>=10.1.1"]
36
36
  build-backend = "setuptools.build_meta"
37
37
 
38
38
  [tool.setuptools_scm]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_openevse_http
3
- Version: 1.3.0
3
+ Version: 1.5.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
@@ -84,6 +84,35 @@ async def main():
84
84
  if __name__ == "__main__":
85
85
  asyncio.run(main())
86
86
  ```
87
+ ### HTTPS and SSL Verification Options
88
+
89
+ If your OpenEVSE WiFi/ethernet module uses HTTPS, you can configure the client to connect securely using the `ssl=True` parameter:
90
+
91
+ ```python
92
+ # Connect securely using HTTPS (validating SSL/TLS certificates)
93
+ charger = OpenEVSE(
94
+ "192.168.1.30",
95
+ session=session,
96
+ ssl=True,
97
+ )
98
+ ```
99
+
100
+ #### Bypassing SSL Verification (Self-Signed Certificates)
101
+
102
+ > [!WARNING]
103
+ > Disabling SSL certificate validation (`ssl_verify=False`) disables TLS verification and exposes the connection to Man-in-the-Middle (MITM) attacks. Only use this configuration when connecting to an OpenEVSE module with a self-signed certificate over a trusted local network.
104
+
105
+ To bypass certificate verification:
106
+
107
+ ```python
108
+ # Connect using HTTPS, and disable certificate verification
109
+ charger = OpenEVSE(
110
+ "192.168.1.30",
111
+ session=session,
112
+ ssl=True,
113
+ ssl_verify=False,
114
+ )
115
+ ```
87
116
 
88
117
  ## API Support Matrix
89
118
 
@@ -13,7 +13,11 @@ requirements_test.txt
13
13
  setup.py
14
14
  tox.ini
15
15
  .github/dependabot.yml
16
+ .github/pull_request_template.md
16
17
  .github/release-drafter.yml
18
+ .github/ISSUE_TEMPLATE/bug_report.yml
19
+ .github/ISSUE_TEMPLATE/config.yml
20
+ .github/ISSUE_TEMPLATE/feature_request.yml
17
21
  .github/workflows/autolabeler.yml
18
22
  .github/workflows/links.yml
19
23
  .github/workflows/publish-to-pypi.yml
@@ -36,6 +40,8 @@ python_openevse_http.egg-info/SOURCES.txt
36
40
  python_openevse_http.egg-info/dependency_links.txt
37
41
  python_openevse_http.egg-info/not-zip-safe
38
42
  python_openevse_http.egg-info/requires.txt
43
+ python_openevse_http.egg-info/scm_file_list.json
44
+ python_openevse_http.egg-info/scm_version.json
39
45
  python_openevse_http.egg-info/top_level.txt
40
46
  tests/__init__.py
41
47
  tests/common.py
@@ -0,0 +1,70 @@
1
+ {
2
+ "files": [
3
+ ".pre-commit-config.yaml",
4
+ ".yamllint",
5
+ "README.md",
6
+ "EXTERNAL_SESSION.md",
7
+ "tox.ini",
8
+ "requirements_lint.txt",
9
+ "requirements_test.txt",
10
+ "LICENSE",
11
+ "setup.py",
12
+ "pyproject.toml",
13
+ "requirements.txt",
14
+ "codecov.yml",
15
+ ".gitignore",
16
+ "example_external_session.py",
17
+ "openevsehttp/py.typed",
18
+ "openevsehttp/__init__.py",
19
+ "openevsehttp/const.py",
20
+ "openevsehttp/exceptions.py",
21
+ "openevsehttp/utils.py",
22
+ "openevsehttp/client.py",
23
+ "openevsehttp/websocket.py",
24
+ "openevsehttp/managers.py",
25
+ "openevsehttp/properties.py",
26
+ "openevsehttp/sensors.py",
27
+ "openevsehttp/commands.py",
28
+ "openevsehttp/__main__.py",
29
+ "tests/__init__.py",
30
+ "tests/test_managers.py",
31
+ "tests/test_client.py",
32
+ "tests/test_main_edge_cases.py",
33
+ "tests/test_shaper.py",
34
+ "tests/test_commands.py",
35
+ "tests/conftest.py",
36
+ "tests/test_external_session.py",
37
+ "tests/test_mixins.py",
38
+ "tests/test_websocket.py",
39
+ "tests/test_properties.py",
40
+ "tests/test_sensors.py",
41
+ "tests/common.py",
42
+ "tests/fixtures/github_v4.json",
43
+ "tests/fixtures/github_v2.json",
44
+ "tests/fixtures/websocket.json",
45
+ "tests/fixtures/v2_json/config.json",
46
+ "tests/fixtures/v2_json/status.json",
47
+ "tests/fixtures/v4_json/config-broken-semver.json",
48
+ "tests/fixtures/v4_json/status-new.json",
49
+ "tests/fixtures/v4_json/status-broken.json",
50
+ "tests/fixtures/v4_json/config-broken.json",
51
+ "tests/fixtures/v4_json/config-dev.json",
52
+ "tests/fixtures/v4_json/schedule.json",
53
+ "tests/fixtures/v4_json/config.json",
54
+ "tests/fixtures/v4_json/config-unknown-semver.json",
55
+ "tests/fixtures/v4_json/config-new.json",
56
+ "tests/fixtures/v4_json/config-extra-version.json",
57
+ "tests/fixtures/v4_json/status.json",
58
+ ".github/release-drafter.yml",
59
+ ".github/pull_request_template.md",
60
+ ".github/dependabot.yml",
61
+ ".github/ISSUE_TEMPLATE/bug_report.yml",
62
+ ".github/ISSUE_TEMPLATE/config.yml",
63
+ ".github/ISSUE_TEMPLATE/feature_request.yml",
64
+ ".github/workflows/test.yml",
65
+ ".github/workflows/autolabeler.yml",
66
+ ".github/workflows/release-drafter.yml",
67
+ ".github/workflows/links.yml",
68
+ ".github/workflows/publish-to-pypi.yml"
69
+ ]
70
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.5.0",
3
+ "distance": 0,
4
+ "node": "g1f850ad2b147d75315538154ce50b42e8ad81319",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-06-23"
8
+ }
@@ -1,4 +1,4 @@
1
1
  -r requirements.txt
2
- prek==0.4.4
3
- ruff==0.15.17
2
+ prek==0.4.5
3
+ ruff==0.15.18
4
4
  mypy==2.1.0
@@ -1,6 +1,6 @@
1
1
  -r requirements.txt
2
- prek==0.4.4
3
- pytest==9.0.3
2
+ prek==0.4.5
3
+ pytest==9.1.1
4
4
  pytest-cov==7.1.0
5
5
  pytest-timeout==2.4.0
6
6
  pytest-asyncio
@@ -1887,3 +1887,44 @@ async def test_get_session_no_running_loop_mocked(charger_factory):
1887
1887
  charger = charger_factory()
1888
1888
  with patch("asyncio.get_running_loop", side_effect=RuntimeError):
1889
1889
  assert charger._get_session() is charger._session
1890
+
1891
+
1892
+ async def test_ssl_options(mock_aioclient):
1893
+ """Test SSL options and SSL verification configuration."""
1894
+ # 1. Default (ssl=False, ssl_verify=True)
1895
+ charger_default = OpenEVSE(SERVER_URL, session=MockClientSession(mock_aioclient))
1896
+ assert charger_default.url == f"http://{SERVER_URL}/"
1897
+ assert charger_default.ssl is False
1898
+ assert charger_default.ssl_verify is True
1899
+
1900
+ # 2. Secure connection (ssl=True, ssl_verify=True)
1901
+ charger_ssl = OpenEVSE(
1902
+ SERVER_URL, ssl=True, session=MockClientSession(mock_aioclient)
1903
+ )
1904
+ assert charger_ssl.url == f"https://{SERVER_URL}/"
1905
+ assert charger_ssl.ssl is True
1906
+ assert charger_ssl.ssl_verify is True
1907
+
1908
+ # 3. Secure connection with disabled validation (ssl=True, ssl_verify=False)
1909
+ charger_no_verify = OpenEVSE(
1910
+ SERVER_URL,
1911
+ ssl=True,
1912
+ ssl_verify=False,
1913
+ session=MockClientSession(mock_aioclient),
1914
+ )
1915
+ assert charger_no_verify.url == f"https://{SERVER_URL}/"
1916
+ assert charger_no_verify.ssl is True
1917
+ assert charger_no_verify.ssl_verify is False
1918
+
1919
+ # Check request parameter passing
1920
+ url = f"https://{SERVER_URL}/status"
1921
+ mock_aioclient.get(url, status=200, body='{"state": "sleeping"}')
1922
+
1923
+ # When ssl_verify=False, ssl=False should be passed to session request kwargs
1924
+ await charger_no_verify.process_request(url, method="get")
1925
+ assert mock_aioclient.requests[-1][2].get("ssl") is False
1926
+
1927
+ # When ssl_verify=True, ssl should NOT be passed as False
1928
+ mock_aioclient.get(url, status=200, body='{"state": "sleeping"}')
1929
+ await charger_ssl.process_request(url, method="get")
1930
+ assert "ssl" not in mock_aioclient.requests[-1][2]
@@ -1334,3 +1334,36 @@ async def test_update_firmware_bytes_empty(test_charger, caplog):
1334
1334
  with pytest.raises(ValueError):
1335
1335
  await test_charger.update_firmware(firmware_bytes=b"")
1336
1336
  assert "Empty firmware bytes provided" in caplog.text
1337
+
1338
+
1339
+ async def test_set_mqtt_vehicle_range_miles(test_charger_new, mock_aioclient, caplog):
1340
+ """Test set_mqtt_vehicle_range_miles command."""
1341
+ await test_charger_new.update()
1342
+ mock_aioclient.post(
1343
+ TEST_URL_CONFIG,
1344
+ status=200,
1345
+ body='{"msg": "OK"}',
1346
+ )
1347
+ with caplog.at_level(logging.DEBUG):
1348
+ await test_charger_new.set_mqtt_vehicle_range_miles(True)
1349
+ assert "Setting mqtt_vehicle_range_miles to True" in caplog.text
1350
+
1351
+ mock_aioclient.post(
1352
+ TEST_URL_CONFIG,
1353
+ status=200,
1354
+ body='{"msg": "OK"}',
1355
+ )
1356
+ with caplog.at_level(logging.DEBUG):
1357
+ await test_charger_new.set_mqtt_vehicle_range_miles(False)
1358
+ assert "Setting mqtt_vehicle_range_miles to False" in caplog.text
1359
+
1360
+ with pytest.raises(TypeError, match=r"Value must be a boolean\."):
1361
+ await test_charger_new.set_mqtt_vehicle_range_miles("invalid")
1362
+
1363
+ mock_aioclient.post(
1364
+ TEST_URL_CONFIG,
1365
+ status=200,
1366
+ body='{"msg": "error"}',
1367
+ )
1368
+ with pytest.raises(UnknownError):
1369
+ await test_charger_new.set_mqtt_vehicle_range_miles(True)
@@ -159,6 +159,8 @@ SERVER_URL = "openevse.test.tld"
159
159
  ("test_charger_v2", "vehicle_soc", None),
160
160
  ("test_charger", "vehicle_range", 468),
161
161
  ("test_charger_v2", "vehicle_range", None),
162
+ ("test_charger", "vehicle_range_with_unit", (468, "km")),
163
+ ("test_charger_v2", "vehicle_range_with_unit", None),
162
164
  # shaper
163
165
  ("test_charger", "shaper_active", True),
164
166
  ("test_charger_v2", "shaper_active", None),
@@ -207,7 +209,11 @@ async def test_simple_properties(fixture, prop, expected, request):
207
209
  with pytest.raises(expected):
208
210
  _ = getattr(charger, prop)
209
211
  else:
210
- assert getattr(charger, prop) == expected
212
+ if prop == "vehicle_range":
213
+ with pytest.deprecated_call():
214
+ assert getattr(charger, prop) == expected
215
+ else:
216
+ assert getattr(charger, prop) == expected
211
217
  await charger.ws_disconnect()
212
218
 
213
219
 
@@ -490,3 +496,23 @@ async def test_wifi_firmware_none():
490
496
  charger = OpenEVSE(SERVER_URL)
491
497
  charger._config = {}
492
498
  assert charger.wifi_firmware is None
499
+
500
+
501
+ async def test_mqtt_vehicle_range_miles():
502
+ """Test mqtt_vehicle_range_miles property and vehicle_range_with_unit unit selection."""
503
+ charger = OpenEVSE(SERVER_URL)
504
+ # Default is False
505
+ assert charger.mqtt_vehicle_range_miles is False
506
+
507
+ charger._config = {"mqtt_vehicle_range_miles": True}
508
+ assert charger.mqtt_vehicle_range_miles is True
509
+
510
+ charger._status = {"vehicle_range": 150}
511
+ with pytest.deprecated_call():
512
+ assert charger.vehicle_range == 150
513
+ assert charger.vehicle_range_with_unit == (150, "miles")
514
+
515
+ charger._config = {"mqtt_vehicle_range_miles": False}
516
+ with pytest.deprecated_call():
517
+ assert charger.vehicle_range == 150
518
+ assert charger.vehicle_range_with_unit == (150, "km")
@@ -548,3 +548,55 @@ async def test_websocket_close_cancels_pending_tasks(ws_client):
548
548
  # Close should cancel and drain tasks
549
549
  await ws_client.close()
550
550
  assert len(ws_client._tasks) == 0
551
+
552
+
553
+ @pytest.mark.asyncio
554
+ async def test_websocket_ssl_options(mock_callback):
555
+ """Test OpenEVSEWebsocket SSL options and ws_connect parameter passing."""
556
+ async with aiohttp.ClientSession() as session:
557
+ # Default ssl_verify=True
558
+ ws_default = OpenEVSEWebsocket(
559
+ "https://openevse.test.tld/",
560
+ mock_callback,
561
+ session=session,
562
+ )
563
+ assert ws_default.ssl_verify is True
564
+ assert ws_default.uri == "wss://openevse.test.tld/ws"
565
+
566
+ # Explicit ssl_verify=False
567
+ ws_no_verify = OpenEVSEWebsocket(
568
+ "https://openevse.test.tld/",
569
+ mock_callback,
570
+ session=session,
571
+ ssl_verify=False,
572
+ )
573
+ assert ws_no_verify.ssl_verify is False
574
+ assert ws_no_verify.uri == "wss://openevse.test.tld/ws"
575
+
576
+ # Mock ws_connect to check that it is called with ssl=False when ssl_verify=False
577
+ mock_ws = AsyncMock()
578
+ mock_ws.__aenter__ = AsyncMock(return_value=mock_ws)
579
+ mock_ws.__aexit__ = AsyncMock(return_value=None)
580
+
581
+ async def empty_iter():
582
+ return
583
+ yield
584
+
585
+ mock_ws.__aiter__.side_effect = empty_iter
586
+
587
+ with patch(
588
+ "aiohttp.ClientSession.ws_connect", return_value=mock_ws
589
+ ) as mock_connect:
590
+ await ws_no_verify.running()
591
+ mock_connect.assert_called_once()
592
+ call_kwargs = mock_connect.call_args.kwargs
593
+ assert call_kwargs.get("ssl") is False
594
+
595
+ # When ssl_verify=True, ws_connect should NOT pass ssl=False
596
+ with patch(
597
+ "aiohttp.ClientSession.ws_connect", return_value=mock_ws
598
+ ) as mock_connect:
599
+ await ws_default.running()
600
+ mock_connect.assert_called_once()
601
+ call_kwargs = mock_connect.call_args.kwargs
602
+ assert "ssl" not in call_kwargs