python-openevse-http 0.3.5__tar.gz → 0.4.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 (70) hide show
  1. python_openevse_http-0.4.0/.github/workflows/publish-to-pypi.yml +46 -0
  2. {python_openevse_http-0.3.5/python_openevse_http.egg-info → python_openevse_http-0.4.0}/PKG-INFO +1 -1
  3. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/openevsehttp/commands.py +67 -0
  4. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0/python_openevse_http.egg-info}/PKG-INFO +1 -1
  5. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/test_commands.py +132 -0
  6. python_openevse_http-0.3.5/.github/workflows/publish-to-pypi.yml +0 -35
  7. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/.github/dependabot.yml +0 -0
  8. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/.github/release-drafter.yml +0 -0
  9. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/.github/workflows/autolabeler.yml +0 -0
  10. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/.github/workflows/links.yml +0 -0
  11. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/.github/workflows/release-drafter.yml +0 -0
  12. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/.github/workflows/test.yml +0 -0
  13. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/.gitignore +0 -0
  14. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/.pre-commit-config.yaml +0 -0
  15. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/.yamllint +0 -0
  16. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/EXTERNAL_SESSION.md +0 -0
  17. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/LICENSE +0 -0
  18. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/README.md +0 -0
  19. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/codecov.yml +0 -0
  20. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/example_external_session.py +0 -0
  21. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/openevsehttp/__init__.py +0 -0
  22. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/openevsehttp/__main__.py +0 -0
  23. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/openevsehttp/client.py +0 -0
  24. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/openevsehttp/const.py +0 -0
  25. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/openevsehttp/exceptions.py +0 -0
  26. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/openevsehttp/managers.py +0 -0
  27. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/openevsehttp/properties.py +0 -0
  28. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/openevsehttp/sensors.py +0 -0
  29. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/openevsehttp/utils.py +0 -0
  30. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/openevsehttp/websocket.py +0 -0
  31. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/pyproject.toml +0 -0
  32. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/python_openevse_http.egg-info/SOURCES.txt +0 -0
  33. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/python_openevse_http.egg-info/dependency_links.txt +0 -0
  34. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/python_openevse_http.egg-info/not-zip-safe +0 -0
  35. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/python_openevse_http.egg-info/requires.txt +0 -0
  36. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/python_openevse_http.egg-info/top_level.txt +0 -0
  37. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/requirements.txt +0 -0
  38. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/requirements_lint.txt +0 -0
  39. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/requirements_test.txt +0 -0
  40. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/setup.cfg +0 -0
  41. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/setup.py +0 -0
  42. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/__init__.py +0 -0
  43. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/common.py +0 -0
  44. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/conftest.py +0 -0
  45. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/fixtures/github_v2.json +0 -0
  46. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/fixtures/github_v4.json +0 -0
  47. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/fixtures/v2_json/config.json +0 -0
  48. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/fixtures/v2_json/status.json +0 -0
  49. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/fixtures/v4_json/config-broken-semver.json +0 -0
  50. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/fixtures/v4_json/config-broken.json +0 -0
  51. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/fixtures/v4_json/config-dev.json +0 -0
  52. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/fixtures/v4_json/config-extra-version.json +0 -0
  53. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/fixtures/v4_json/config-new.json +0 -0
  54. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/fixtures/v4_json/config-unknown-semver.json +0 -0
  55. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/fixtures/v4_json/config.json +0 -0
  56. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/fixtures/v4_json/schedule.json +0 -0
  57. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/fixtures/v4_json/status-broken.json +0 -0
  58. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/fixtures/v4_json/status-new.json +0 -0
  59. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/fixtures/v4_json/status.json +0 -0
  60. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/fixtures/websocket.json +0 -0
  61. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/test_client.py +0 -0
  62. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/test_external_session.py +0 -0
  63. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/test_main_edge_cases.py +0 -0
  64. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/test_managers.py +0 -0
  65. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/test_mixins.py +0 -0
  66. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/test_properties.py +0 -0
  67. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/test_sensors.py +0 -0
  68. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/test_shaper.py +0 -0
  69. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tests/test_websocket.py +0 -0
  70. {python_openevse_http-0.3.5 → python_openevse_http-0.4.0}/tox.ini +0 -0
@@ -0,0 +1,46 @@
1
+ name: Publish releases to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published, prereleased]
6
+ workflow_dispatch:
7
+ inputs:
8
+ tag:
9
+ description: 'Tag/Ref to build and publish'
10
+ required: false
11
+ default: ''
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ build-and-publish:
18
+ name: Builds and publishes releases to PyPI
19
+ runs-on: ubuntu-latest
20
+ permissions:
21
+ contents: read
22
+ id-token: write
23
+ steps:
24
+ - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
25
+ with:
26
+ egress-policy: audit
27
+
28
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
29
+ with:
30
+ ref: ${{ inputs.tag || github.ref }}
31
+ fetch-depth: 0
32
+
33
+ - name: Install uv
34
+ uses: astral-sh/setup-uv@1edb52594c857e2b5b13128931090f0640537287 # v5.3.0
35
+ with:
36
+ enable-cache: true
37
+ version: "0.10.9"
38
+
39
+ - name: Set up Python
40
+ run: uv python install 3.14
41
+
42
+ - name: Build package
43
+ run: uv build
44
+
45
+ - name: Publish release to PyPI
46
+ uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_openevse_http
3
- Version: 0.3.5
3
+ Version: 0.4.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
@@ -414,12 +414,79 @@ class CommandsMixin:
414
414
 
415
415
  if not isinstance(message, dict):
416
416
  return None
417
+
418
+ # Match browser_download_url based on buildenv
419
+ download_url = None
420
+ buildenv = self._config.get("buildenv")
421
+ assets = message.get("assets", [])
422
+
423
+ if buildenv and assets:
424
+ target_filename = f"{buildenv}.bin"
425
+ for asset in assets:
426
+ if asset.get("name") == target_filename:
427
+ download_url = asset.get("browser_download_url")
428
+ break
429
+
417
430
  return {
418
431
  "latest_version": message.get("tag_name"),
419
432
  "release_notes": message.get("body"),
420
433
  "release_url": message.get("html_url"),
434
+ "browser_download_url": download_url,
421
435
  }
422
436
 
437
+ async def update_firmware(
438
+ self,
439
+ firmware_url: str | None = None,
440
+ firmware_bytes: bytes | None = None,
441
+ filename: str = "firmware.bin",
442
+ ) -> Mapping[str, Any] | list[Any] | str:
443
+ """Instruct the device to update its firmware.
444
+
445
+ You can either:
446
+ 1. Pass firmware_bytes to perform a multipart upload of a local file.
447
+ 2. Pass firmware_url to tell the device to download the file directly.
448
+ 3. Pass neither to automatically resolve the latest matching binary URL from GitHub.
449
+ """
450
+ if firmware_bytes is not None and firmware_url is not None:
451
+ raise ValueError("Cannot specify both firmware_bytes and firmware_url")
452
+
453
+ if firmware_url is not None:
454
+ if not isinstance(firmware_url, str) or not firmware_url.strip():
455
+ raise ValueError("Invalid firmware_url")
456
+
457
+ url = f"{self.url}update"
458
+
459
+ # 1. Handle multipart binary upload
460
+ if firmware_bytes is not None:
461
+ form_data = aiohttp.FormData()
462
+ form_data.add_field(
463
+ name="file",
464
+ value=firmware_bytes,
465
+ filename=filename,
466
+ content_type="application/octet-stream",
467
+ )
468
+ _LOGGER.debug(
469
+ "Uploading firmware binary to %s (%d bytes)", url, len(firmware_bytes)
470
+ )
471
+ # 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)
473
+
474
+ # 2. Resolve URL from GitHub if not specified
475
+ if firmware_url is None:
476
+ check_result = await self.firmware_check()
477
+ if not check_result or not check_result.get("browser_download_url"):
478
+ raise RuntimeError(
479
+ "Could not resolve latest firmware download URL from GitHub."
480
+ )
481
+ firmware_url = check_result["browser_download_url"]
482
+
483
+ # 3. Post JSON URL payload
484
+ data = {"url": firmware_url}
485
+ _LOGGER.debug(
486
+ "Requesting OpenEVSE to download and update from: %s", firmware_url
487
+ )
488
+ return await self.process_request(url=url, method="post", data=data)
489
+
423
490
  async def set_led_brightness(self, level: int) -> None:
424
491
  """Set LED brightness level."""
425
492
  if isinstance(level, bool) or not isinstance(level, int):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_openevse_http
3
- Version: 0.3.5
3
+ Version: 0.4.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
@@ -1040,3 +1040,135 @@ async def test_normalize_response(test_charger):
1040
1040
  assert test_charger._normalize_response({"msg": "OK"}) == {"msg": "OK"}
1041
1041
  # Test with string
1042
1042
  assert test_charger._normalize_response("OK") == {"msg": "OK"}
1043
+
1044
+
1045
+ # ── update_firmware ──────────────────────────────────────────────────
1046
+
1047
+
1048
+ async def test_update_firmware_bytes(test_charger, mock_aioclient, caplog):
1049
+ """Test update_firmware with bytes upload."""
1050
+ mock_aioclient.post(
1051
+ "http://openevse.test.tld/update",
1052
+ status=200,
1053
+ body="OK",
1054
+ )
1055
+ with caplog.at_level(logging.DEBUG):
1056
+ response = await test_charger.update_firmware(firmware_bytes=b"fakebinarydata")
1057
+ assert response == "OK"
1058
+ assert (
1059
+ "Uploading firmware binary to http://openevse.test.tld/update (14 bytes)"
1060
+ in caplog.text
1061
+ )
1062
+
1063
+
1064
+ async def test_update_firmware_url(test_charger, mock_aioclient, caplog):
1065
+ """Test update_firmware with a direct URL."""
1066
+ mock_aioclient.post(
1067
+ "http://openevse.test.tld/update",
1068
+ status=200,
1069
+ body='{"msg":"started"}',
1070
+ )
1071
+ with caplog.at_level(logging.DEBUG):
1072
+ response = await test_charger.update_firmware(
1073
+ firmware_url="http://github.com/release.bin"
1074
+ )
1075
+ assert response == {"msg": "started"}
1076
+ assert (
1077
+ "Requesting OpenEVSE to download and update from: http://github.com/release.bin"
1078
+ in caplog.text
1079
+ )
1080
+
1081
+
1082
+ async def test_update_firmware_auto(test_charger, mock_aioclient, caplog):
1083
+ """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"}
1086
+
1087
+ # Mock GitHub Releases API to return assets matching buildenv
1088
+ github_response = {
1089
+ "tag_name": "v4.1.2",
1090
+ "body": "release notes",
1091
+ "html_url": "https://github.com/OpenEVSE/releases/v4.1.2",
1092
+ "assets": [
1093
+ {
1094
+ "name": "openevse_esp32-gateway.bin",
1095
+ "browser_download_url": "https://github.com/OpenEVSE/releases/download/v4.1.2/openevse_esp32-gateway.bin",
1096
+ },
1097
+ {
1098
+ "name": "other_env.bin",
1099
+ "browser_download_url": "https://github.com/OpenEVSE/releases/download/v4.1.2/other_env.bin",
1100
+ },
1101
+ ],
1102
+ }
1103
+
1104
+ mock_aioclient.get(
1105
+ "https://api.github.com/repos/OpenEVSE/ESP32_WiFi_V4.x/releases/latest",
1106
+ status=200,
1107
+ body=json.dumps(github_response),
1108
+ )
1109
+
1110
+ mock_aioclient.post(
1111
+ "http://openevse.test.tld/update",
1112
+ status=200,
1113
+ body='{"msg":"started"}',
1114
+ )
1115
+
1116
+ with caplog.at_level(logging.DEBUG):
1117
+ response = await test_charger.update_firmware()
1118
+ assert response == {"msg": "started"}
1119
+ assert (
1120
+ "Requesting OpenEVSE to download and update from: https://github.com/OpenEVSE/releases/download/v4.1.2/openevse_esp32-gateway.bin"
1121
+ in caplog.text
1122
+ )
1123
+
1124
+
1125
+ async def test_update_firmware_auto_missing_buildenv(test_charger, mock_aioclient):
1126
+ """Test update_firmware raises RuntimeError when buildenv asset is missing."""
1127
+ test_charger._config = {"version": "4.0.1", "buildenv": "openevse_esp32-gateway"}
1128
+
1129
+ # Mock GitHub releases but without the matching gateway asset
1130
+ github_response = {
1131
+ "tag_name": "v4.1.2",
1132
+ "body": "release notes",
1133
+ "html_url": "https://github.com/OpenEVSE/releases/v4.1.2",
1134
+ "assets": [
1135
+ {
1136
+ "name": "other_env.bin",
1137
+ "browser_download_url": "https://github.com/OpenEVSE/releases/download/v4.1.2/other_env.bin",
1138
+ }
1139
+ ],
1140
+ }
1141
+
1142
+ mock_aioclient.get(
1143
+ "https://api.github.com/repos/OpenEVSE/ESP32_WiFi_V4.x/releases/latest",
1144
+ status=200,
1145
+ body=json.dumps(github_response),
1146
+ )
1147
+
1148
+ with pytest.raises(
1149
+ RuntimeError,
1150
+ match="Could not resolve latest firmware download URL from GitHub.",
1151
+ ):
1152
+ await test_charger.update_firmware()
1153
+
1154
+
1155
+ async def test_update_firmware_both_provided(test_charger):
1156
+ """Test update_firmware raises ValueError when both bytes and URL are provided."""
1157
+ with pytest.raises(
1158
+ ValueError, match="Cannot specify both firmware_bytes and firmware_url"
1159
+ ):
1160
+ await test_charger.update_firmware(
1161
+ firmware_url="http://url", firmware_bytes=b"bytes"
1162
+ )
1163
+
1164
+
1165
+ async def test_update_firmware_url_invalid(test_charger):
1166
+ """Test update_firmware raises ValueError when firmware_url is empty or invalid type."""
1167
+ with pytest.raises(ValueError, match="Invalid firmware_url"):
1168
+ await test_charger.update_firmware(firmware_url="")
1169
+
1170
+ with pytest.raises(ValueError, match="Invalid firmware_url"):
1171
+ await test_charger.update_firmware(firmware_url=" ")
1172
+
1173
+ with pytest.raises(ValueError, match="Invalid firmware_url"):
1174
+ await test_charger.update_firmware(firmware_url=123) # type: ignore
@@ -1,35 +0,0 @@
1
- name: Publish releases to PyPI
2
-
3
- on:
4
- release:
5
- types: [published, prereleased]
6
- workflow_dispatch:
7
-
8
- permissions:
9
- contents: read
10
-
11
- jobs:
12
- build-and-publish:
13
- name: Builds and publishes releases to PyPI
14
- runs-on: ubuntu-latest
15
- steps:
16
- - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
17
- with:
18
- egress-policy: audit
19
-
20
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
21
- - name: Set up Python 3.9
22
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
23
- with:
24
- python-version: 3.9
25
- - name: Install wheel
26
- run: >-
27
- pip install wheel
28
- - name: Build
29
- run: >-
30
- python3 setup.py sdist bdist_wheel
31
- - name: Publish release to PyPI
32
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
33
- with:
34
- user: __token__
35
- password: ${{ secrets.PYPI_TOKEN }}