python-openevse-http 0.4.0__tar.gz → 0.4.2__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 (69) hide show
  1. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/.github/workflows/autolabeler.yml +1 -1
  2. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/.github/workflows/links.yml +1 -1
  3. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/.github/workflows/publish-to-pypi.yml +3 -3
  4. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/.github/workflows/release-drafter.yml +1 -1
  5. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/.github/workflows/test.yml +3 -3
  6. {python_openevse_http-0.4.0/python_openevse_http.egg-info → python_openevse_http-0.4.2}/PKG-INFO +1 -1
  7. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/openevsehttp/client.py +13 -3
  8. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/openevsehttp/commands.py +22 -3
  9. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/openevsehttp/properties.py +11 -1
  10. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/pyproject.toml +1 -1
  11. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2/python_openevse_http.egg-info}/PKG-INFO +1 -1
  12. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/requirements_test.txt +1 -1
  13. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/test_client.py +72 -0
  14. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/test_commands.py +33 -3
  15. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/test_properties.py +16 -2
  16. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/.github/dependabot.yml +0 -0
  17. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/.github/release-drafter.yml +0 -0
  18. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/.gitignore +0 -0
  19. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/.pre-commit-config.yaml +0 -0
  20. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/.yamllint +0 -0
  21. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/EXTERNAL_SESSION.md +0 -0
  22. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/LICENSE +0 -0
  23. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/README.md +0 -0
  24. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/codecov.yml +0 -0
  25. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/example_external_session.py +0 -0
  26. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/openevsehttp/__init__.py +0 -0
  27. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/openevsehttp/__main__.py +0 -0
  28. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/openevsehttp/const.py +0 -0
  29. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/openevsehttp/exceptions.py +0 -0
  30. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/openevsehttp/managers.py +0 -0
  31. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/openevsehttp/sensors.py +0 -0
  32. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/openevsehttp/utils.py +0 -0
  33. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/openevsehttp/websocket.py +0 -0
  34. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/python_openevse_http.egg-info/SOURCES.txt +0 -0
  35. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/python_openevse_http.egg-info/dependency_links.txt +0 -0
  36. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/python_openevse_http.egg-info/not-zip-safe +0 -0
  37. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/python_openevse_http.egg-info/requires.txt +0 -0
  38. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/python_openevse_http.egg-info/top_level.txt +0 -0
  39. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/requirements.txt +0 -0
  40. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/requirements_lint.txt +0 -0
  41. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/setup.cfg +0 -0
  42. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/setup.py +0 -0
  43. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/__init__.py +0 -0
  44. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/common.py +0 -0
  45. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/conftest.py +0 -0
  46. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/fixtures/github_v2.json +0 -0
  47. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/fixtures/github_v4.json +0 -0
  48. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/fixtures/v2_json/config.json +0 -0
  49. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/fixtures/v2_json/status.json +0 -0
  50. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/fixtures/v4_json/config-broken-semver.json +0 -0
  51. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/fixtures/v4_json/config-broken.json +0 -0
  52. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/fixtures/v4_json/config-dev.json +0 -0
  53. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/fixtures/v4_json/config-extra-version.json +0 -0
  54. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/fixtures/v4_json/config-new.json +0 -0
  55. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/fixtures/v4_json/config-unknown-semver.json +0 -0
  56. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/fixtures/v4_json/config.json +0 -0
  57. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/fixtures/v4_json/schedule.json +0 -0
  58. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/fixtures/v4_json/status-broken.json +0 -0
  59. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/fixtures/v4_json/status-new.json +0 -0
  60. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/fixtures/v4_json/status.json +0 -0
  61. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/fixtures/websocket.json +0 -0
  62. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/test_external_session.py +0 -0
  63. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/test_main_edge_cases.py +0 -0
  64. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/test_managers.py +0 -0
  65. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/test_mixins.py +0 -0
  66. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/test_sensors.py +0 -0
  67. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/test_shaper.py +0 -0
  68. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tests/test_websocket.py +0 -0
  69. {python_openevse_http-0.4.0 → python_openevse_http-0.4.2}/tox.ini +0 -0
@@ -32,7 +32,7 @@ jobs:
32
32
  runs-on: ubuntu-latest
33
33
  timeout-minutes: 3
34
34
  steps:
35
- - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
35
+ - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
36
36
  with:
37
37
  egress-policy: audit
38
38
 
@@ -15,7 +15,7 @@ jobs:
15
15
  linkChecker:
16
16
  runs-on: ubuntu-latest
17
17
  steps:
18
- - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
18
+ - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
19
19
  with:
20
20
  egress-policy: audit
21
21
 
@@ -21,7 +21,7 @@ jobs:
21
21
  contents: read
22
22
  id-token: write
23
23
  steps:
24
- - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
24
+ - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
25
25
  with:
26
26
  egress-policy: audit
27
27
 
@@ -31,7 +31,7 @@ jobs:
31
31
  fetch-depth: 0
32
32
 
33
33
  - name: Install uv
34
- uses: astral-sh/setup-uv@1edb52594c857e2b5b13128931090f0640537287 # v5.3.0
34
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
35
35
  with:
36
36
  enable-cache: true
37
37
  version: "0.10.9"
@@ -43,4 +43,4 @@ jobs:
43
43
  run: uv build
44
44
 
45
45
  - name: Publish release to PyPI
46
- uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
46
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
@@ -13,7 +13,7 @@ jobs:
13
13
  update_release_draft:
14
14
  runs-on: ubuntu-latest
15
15
  steps:
16
- - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
16
+ - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
17
17
  with:
18
18
  egress-policy: audit
19
19
 
@@ -18,7 +18,7 @@ jobs:
18
18
  prek:
19
19
  runs-on: ubuntu-latest
20
20
  steps:
21
- - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
21
+ - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
22
22
  with:
23
23
  egress-policy: audit
24
24
 
@@ -41,7 +41,7 @@ jobs:
41
41
  - "3.14"
42
42
 
43
43
  steps:
44
- - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
44
+ - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
45
45
  with:
46
46
  egress-policy: audit
47
47
 
@@ -69,7 +69,7 @@ jobs:
69
69
  runs-on: ubuntu-latest
70
70
  needs: build
71
71
  steps:
72
- - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
72
+ - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
73
73
  with:
74
74
  egress-policy: audit
75
75
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_openevse_http
3
- Version: 0.4.0
3
+ Version: 0.4.2
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
@@ -183,12 +183,12 @@ class OpenEVSE(CommandsMixin, ManagersMixin, SensorsMixin, PropertiesMixin):
183
183
  return (False, "")
184
184
  return (value["cmd"], value["ret"])
185
185
 
186
- async def update(self) -> None:
186
+ async def update(self, force_status: bool = False) -> None:
187
187
  """Update the values."""
188
188
  # TODO: add addiontal endpoints to update
189
189
  urls = [f"{self.url}config"]
190
190
 
191
- if not self._ws_listening:
191
+ if not self._ws_listening or force_status or self.ota_update:
192
192
  urls = [f"{self.url}status", f"{self.url}config"]
193
193
 
194
194
  for url in urls:
@@ -196,7 +196,7 @@ class OpenEVSE(CommandsMixin, ManagersMixin, SensorsMixin, PropertiesMixin):
196
196
  response = await self.process_request(url, method="get")
197
197
  if "/status" in url:
198
198
  if isinstance(response, Mapping) and "error" not in response:
199
- self._status = dict(response)
199
+ self._status.update(dict(response))
200
200
  _LOGGER.debug("Status update: %s", self._status)
201
201
  elif isinstance(response, Mapping):
202
202
  _LOGGER.warning(
@@ -337,6 +337,16 @@ class OpenEVSE(CommandsMixin, ManagersMixin, SensorsMixin, PropertiesMixin):
337
337
  # TODO: update specific endpoints based on _version prefix
338
338
  if any(key in keys for key in UPDATE_TRIGGERS):
339
339
  await self.update()
340
+
341
+ if "ota" in keys:
342
+ ota_val = data["ota"]
343
+ if ota_val == "started":
344
+ self._status["ota_update"] = 1
345
+ elif ota_val in ("completed", "failed"):
346
+ self._status["ota_update"] = 0
347
+ data.pop("ota_progress", None)
348
+ self._status.pop("ota_progress", None)
349
+
340
350
  self._status.update(data)
341
351
 
342
352
  if self.callback is not None:
@@ -39,13 +39,22 @@ class CommandsMixin:
39
39
  async def send_command(self, command: str) -> tuple:
40
40
  raise NotImplementedError
41
41
 
42
- async def update(self) -> None:
42
+ async def update(self, force_status: bool = False) -> None:
43
43
  raise NotImplementedError
44
44
 
45
45
  def _normalize_response(self, response: Any) -> dict[str, Any] | list[Any]:
46
46
  """Normalize response to a dict or list."""
47
47
  raise NotImplementedError
48
48
 
49
+ def _flag_ota_if_started(self, response: Any) -> None:
50
+ """Flag OTA as active if response indicates firmware update has started."""
51
+ normalized = self._normalize_response(response)
52
+ if isinstance(normalized, dict) and (
53
+ normalized.get("msg") == "started"
54
+ or normalized.get("msg") in SUCCESS_ANSWERS
55
+ ):
56
+ self._status["ota_update"] = 1
57
+
49
58
  async def get_schedule(self) -> Mapping[str, Any] | list[Any]:
50
59
  """Return the current schedule."""
51
60
  url = f"{self.url}schedule"
@@ -447,6 +456,10 @@ class CommandsMixin:
447
456
  2. Pass firmware_url to tell the device to download the file directly.
448
457
  3. Pass neither to automatically resolve the latest matching binary URL from GitHub.
449
458
  """
459
+ if not self._version_check("4.1.7"):
460
+ _LOGGER.debug("Feature not supported for older firmware.")
461
+ raise UnsupportedFeature
462
+
450
463
  if firmware_bytes is not None and firmware_url is not None:
451
464
  raise ValueError("Cannot specify both firmware_bytes and firmware_url")
452
465
 
@@ -469,7 +482,11 @@ class CommandsMixin:
469
482
  "Uploading firmware binary to %s (%d bytes)", url, len(firmware_bytes)
470
483
  )
471
484
  # Rapi is mapped to http request's data kwarg in process_request
472
- return await self.process_request(url=url, method="post", rapi=form_data)
485
+ response = await self.process_request(
486
+ url=url, method="post", rapi=form_data
487
+ )
488
+ self._flag_ota_if_started(response)
489
+ return response
473
490
 
474
491
  # 2. Resolve URL from GitHub if not specified
475
492
  if firmware_url is None:
@@ -485,7 +502,9 @@ class CommandsMixin:
485
502
  _LOGGER.debug(
486
503
  "Requesting OpenEVSE to download and update from: %s", firmware_url
487
504
  )
488
- return await self.process_request(url=url, method="post", data=data)
505
+ response = await self.process_request(url=url, method="post", data=data)
506
+ self._flag_ota_if_started(response)
507
+ return response
489
508
 
490
509
  async def set_led_brightness(self, level: int) -> None:
491
510
  """Set LED brightness level."""
@@ -341,7 +341,17 @@ class PropertiesMixin:
341
341
  @property
342
342
  def ota_update(self) -> bool:
343
343
  """Return if an OTA update is active."""
344
- return self._status.get("ota_update", False)
344
+ return bool(self._status.get("ota_update", False))
345
+
346
+ @property
347
+ def ota_progress(self) -> int | None:
348
+ """Return the progress of the current OTA update."""
349
+ return self._status.get("ota_progress")
350
+
351
+ @property
352
+ def ota_state(self) -> str | None:
353
+ """Return the state of the current OTA update."""
354
+ return self._status.get("ota")
345
355
 
346
356
  @property
347
357
  def manual_override(self) -> bool:
@@ -32,7 +32,7 @@ ignore = [
32
32
  max-complexity = 18
33
33
 
34
34
  [build-system]
35
- requires = ["setuptools>=61.0.0", "setuptools-scm>=8.0"]
35
+ requires = ["setuptools>=82.0.1", "setuptools-scm>=10.0.5"]
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: 0.4.0
3
+ Version: 0.4.2
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
@@ -5,7 +5,7 @@ pytest-cov==7.1.0
5
5
  pytest-timeout==2.4.0
6
6
  pytest-asyncio
7
7
  requests_mock
8
- aiohttp
8
+ aiohttp<3.11
9
9
  aioresponses
10
10
  tox==4.55.0
11
11
  freezegun
@@ -117,6 +117,55 @@ async def test_get_status_auth_err(test_charger_auth_err):
117
117
  await test_charger_auth_err.update()
118
118
 
119
119
 
120
+ async def test_update_force_status(mock_aioclient):
121
+ """Test force_status parameter when ws is listening."""
122
+ unique_host = "force-status.test.tld"
123
+ charger = OpenEVSE(unique_host)
124
+ url_status = f"http://{unique_host}/status"
125
+ url_config = f"http://{unique_host}/config"
126
+
127
+ mock_aioclient.get(
128
+ url_status,
129
+ status=200,
130
+ body='{"state": 2, "wifi_serial": "123"}',
131
+ )
132
+ mock_aioclient.get(
133
+ url_config,
134
+ status=200,
135
+ body='{"wifi_serial": "123", "version": "4.0.1"}',
136
+ )
137
+ charger._ws_listening = True
138
+ charger._status = {"transient_key": "preserved"}
139
+ await charger.update(force_status=True)
140
+ assert charger._status["state"] == 2
141
+ assert charger._status["transient_key"] == "preserved"
142
+
143
+
144
+ async def test_update_ota_active(mock_aioclient):
145
+ """Test automatic status polling when ota_update is active."""
146
+ unique_host = "ota-active.test.tld"
147
+ charger = OpenEVSE(unique_host)
148
+ url_status = f"http://{unique_host}/status"
149
+ url_config = f"http://{unique_host}/config"
150
+
151
+ mock_aioclient.get(
152
+ url_status,
153
+ status=200,
154
+ body='{"state": 2, "ota_update": 1}',
155
+ )
156
+ mock_aioclient.get(
157
+ url_config,
158
+ status=200,
159
+ body='{"wifi_serial": "123", "version": "4.0.1"}',
160
+ )
161
+ charger._ws_listening = True
162
+ charger._status = {"ota_update": 1, "ota_progress": 50}
163
+ await charger.update()
164
+ assert charger._status["state"] == 2
165
+ assert charger._status["ota_progress"] == 50
166
+ assert charger.ota_update is True
167
+
168
+
120
169
  # ── send_command ──────────────────────────────────────────────────────
121
170
 
122
171
 
@@ -1651,3 +1700,26 @@ async def test_update_status_non_mapping_data(caplog):
1651
1700
  with caplog.at_level(logging.WARNING):
1652
1701
  await charger._update_status("data", "not a dict", None)
1653
1702
  assert "Received non-Mapping websocket data: not a dict" in caplog.text
1703
+
1704
+
1705
+ async def test_update_status_ota():
1706
+ """Test _update_status with ota websocket events."""
1707
+ charger = OpenEVSE(SERVER_URL)
1708
+ charger._status = {"ota_update": 0}
1709
+
1710
+ # 1. Started event
1711
+ await charger._update_status("data", {"ota": "started"}, None)
1712
+ assert charger.ota_update is True
1713
+ assert charger.ota_state == "started"
1714
+
1715
+ # 2. Progress event
1716
+ await charger._update_status("data", {"ota_progress": 25}, None)
1717
+ assert charger.ota_progress == 25
1718
+
1719
+ # 3. Completed event (verifying ota_progress is cleared even if present in the data dict)
1720
+ await charger._update_status(
1721
+ "data", {"ota": "completed", "ota_progress": 100}, None
1722
+ )
1723
+ assert charger.ota_update is False
1724
+ assert charger.ota_progress is None
1725
+ assert charger.ota_state == "completed"
@@ -1047,6 +1047,7 @@ async def test_normalize_response(test_charger):
1047
1047
 
1048
1048
  async def test_update_firmware_bytes(test_charger, mock_aioclient, caplog):
1049
1049
  """Test update_firmware with bytes upload."""
1050
+ test_charger._config["version"] = "4.1.7"
1050
1051
  mock_aioclient.post(
1051
1052
  "http://openevse.test.tld/update",
1052
1053
  status=200,
@@ -1059,10 +1060,12 @@ async def test_update_firmware_bytes(test_charger, mock_aioclient, caplog):
1059
1060
  "Uploading firmware binary to http://openevse.test.tld/update (14 bytes)"
1060
1061
  in caplog.text
1061
1062
  )
1063
+ assert test_charger.ota_update is True
1062
1064
 
1063
1065
 
1064
1066
  async def test_update_firmware_url(test_charger, mock_aioclient, caplog):
1065
1067
  """Test update_firmware with a direct URL."""
1068
+ test_charger._config["version"] = "4.1.7"
1066
1069
  mock_aioclient.post(
1067
1070
  "http://openevse.test.tld/update",
1068
1071
  status=200,
@@ -1077,12 +1080,13 @@ async def test_update_firmware_url(test_charger, mock_aioclient, caplog):
1077
1080
  "Requesting OpenEVSE to download and update from: http://github.com/release.bin"
1078
1081
  in caplog.text
1079
1082
  )
1083
+ assert test_charger.ota_update is True
1080
1084
 
1081
1085
 
1082
1086
  async def test_update_firmware_auto(test_charger, mock_aioclient, caplog):
1083
1087
  """Test update_firmware with auto-resolved URL from GitHub."""
1084
- # Setup config with a buildenv
1085
- test_charger._config = {"version": "4.0.1", "buildenv": "openevse_esp32-gateway"}
1088
+ # Setup config with a buildenv and version >= 4.1.7
1089
+ test_charger._config = {"version": "4.1.7", "buildenv": "openevse_esp32-gateway"}
1086
1090
 
1087
1091
  # Mock GitHub Releases API to return assets matching buildenv
1088
1092
  github_response = {
@@ -1120,11 +1124,12 @@ async def test_update_firmware_auto(test_charger, mock_aioclient, caplog):
1120
1124
  "Requesting OpenEVSE to download and update from: https://github.com/OpenEVSE/releases/download/v4.1.2/openevse_esp32-gateway.bin"
1121
1125
  in caplog.text
1122
1126
  )
1127
+ assert test_charger.ota_update is True
1123
1128
 
1124
1129
 
1125
1130
  async def test_update_firmware_auto_missing_buildenv(test_charger, mock_aioclient):
1126
1131
  """Test update_firmware raises RuntimeError when buildenv asset is missing."""
1127
- test_charger._config = {"version": "4.0.1", "buildenv": "openevse_esp32-gateway"}
1132
+ test_charger._config = {"version": "4.1.7", "buildenv": "openevse_esp32-gateway"}
1128
1133
 
1129
1134
  # Mock GitHub releases but without the matching gateway asset
1130
1135
  github_response = {
@@ -1154,6 +1159,7 @@ async def test_update_firmware_auto_missing_buildenv(test_charger, mock_aioclien
1154
1159
 
1155
1160
  async def test_update_firmware_both_provided(test_charger):
1156
1161
  """Test update_firmware raises ValueError when both bytes and URL are provided."""
1162
+ test_charger._config["version"] = "4.1.7"
1157
1163
  with pytest.raises(
1158
1164
  ValueError, match="Cannot specify both firmware_bytes and firmware_url"
1159
1165
  ):
@@ -1164,6 +1170,7 @@ async def test_update_firmware_both_provided(test_charger):
1164
1170
 
1165
1171
  async def test_update_firmware_url_invalid(test_charger):
1166
1172
  """Test update_firmware raises ValueError when firmware_url is empty or invalid type."""
1173
+ test_charger._config["version"] = "4.1.7"
1167
1174
  with pytest.raises(ValueError, match="Invalid firmware_url"):
1168
1175
  await test_charger.update_firmware(firmware_url="")
1169
1176
 
@@ -1172,3 +1179,26 @@ async def test_update_firmware_url_invalid(test_charger):
1172
1179
 
1173
1180
  with pytest.raises(ValueError, match="Invalid firmware_url"):
1174
1181
  await test_charger.update_firmware(firmware_url=123) # type: ignore
1182
+
1183
+
1184
+ async def test_update_firmware_unsupported(test_charger):
1185
+ """Test update_firmware raises UnsupportedFeature on older firmware."""
1186
+ test_charger._config["version"] = "4.1.2"
1187
+ with pytest.raises(UnsupportedFeature):
1188
+ await test_charger.update_firmware(firmware_url="http://url")
1189
+
1190
+
1191
+ async def test_update_firmware_error_response(test_charger, mock_aioclient):
1192
+ """Test update_firmware doesn't set ota_update on error responses."""
1193
+ test_charger._config["version"] = "4.1.7"
1194
+ test_charger._status = {}
1195
+ mock_aioclient.post(
1196
+ "http://openevse.test.tld/update",
1197
+ status=200,
1198
+ body='{"msg":"error"}',
1199
+ )
1200
+ response = await test_charger.update_firmware(
1201
+ firmware_url="http://github.com/release.bin"
1202
+ )
1203
+ assert response == {"msg": "error"}
1204
+ assert test_charger.ota_update is False
@@ -1040,17 +1040,31 @@ async def test_get_has_limit(fixture, expected, request):
1040
1040
 
1041
1041
 
1042
1042
  @pytest.mark.parametrize(
1043
- "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)]
1043
+ "fixture, expected", [("test_charger", False), ("test_charger_v2", False)]
1044
1044
  )
1045
1045
  async def test_get_ota_update(fixture, expected, request):
1046
1046
  """Test ota_update property."""
1047
1047
  charger = request.getfixturevalue(fixture)
1048
1048
  await charger.update()
1049
1049
  status = charger.ota_update
1050
- assert status == expected
1050
+ assert status is expected
1051
1051
  await charger.ws_disconnect()
1052
1052
 
1053
1053
 
1054
+ async def test_ota_properties():
1055
+ """Test ota_progress and ota_state properties."""
1056
+ charger = OpenEVSE(SERVER_URL)
1057
+ charger._status = {"ota_update": 1, "ota_progress": 45, "ota": "started"}
1058
+ assert charger.ota_update is True
1059
+ assert charger.ota_progress == 45
1060
+ assert charger.ota_state == "started"
1061
+
1062
+ charger._status = {"ota_update": 0}
1063
+ assert charger.ota_update is False
1064
+ assert charger.ota_progress is None
1065
+ assert charger.ota_state is None
1066
+
1067
+
1054
1068
  # ── MQTT ────────────────────────────────────────────────────────────
1055
1069
 
1056
1070