python-openevse-http 1.0.0__tar.gz → 1.0.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 (71) hide show
  1. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/.github/workflows/links.yml +1 -1
  2. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/.github/workflows/publish-to-pypi.yml +2 -2
  3. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/.github/workflows/test.yml +4 -4
  4. {python_openevse_http-1.0.0/python_openevse_http.egg-info → python_openevse_http-1.0.2}/PKG-INFO +1 -1
  5. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/openevsehttp/client.py +4 -4
  6. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/openevsehttp/properties.py +6 -3
  7. python_openevse_http-1.0.2/openevsehttp/utils.py +55 -0
  8. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2/python_openevse_http.egg-info}/PKG-INFO +1 -1
  9. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/requirements_lint.txt +2 -2
  10. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/requirements_test.txt +2 -2
  11. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/fixtures/v4_json/config-unknown-semver.json +1 -1
  12. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/test_client.py +56 -2
  13. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/test_properties.py +2 -2
  14. python_openevse_http-1.0.0/openevsehttp/utils.py +0 -29
  15. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/.github/dependabot.yml +0 -0
  16. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/.github/release-drafter.yml +0 -0
  17. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/.github/workflows/autolabeler.yml +0 -0
  18. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/.github/workflows/release-drafter.yml +0 -0
  19. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/.gitignore +0 -0
  20. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/.pre-commit-config.yaml +0 -0
  21. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/.yamllint +0 -0
  22. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/EXTERNAL_SESSION.md +0 -0
  23. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/LICENSE +0 -0
  24. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/README.md +0 -0
  25. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/codecov.yml +0 -0
  26. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/example_external_session.py +0 -0
  27. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/openevsehttp/__init__.py +0 -0
  28. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/openevsehttp/__main__.py +0 -0
  29. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/openevsehttp/commands.py +0 -0
  30. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/openevsehttp/const.py +0 -0
  31. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/openevsehttp/exceptions.py +0 -0
  32. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/openevsehttp/managers.py +0 -0
  33. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/openevsehttp/py.typed +0 -0
  34. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/openevsehttp/sensors.py +0 -0
  35. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/openevsehttp/websocket.py +0 -0
  36. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/pyproject.toml +0 -0
  37. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/python_openevse_http.egg-info/SOURCES.txt +0 -0
  38. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/python_openevse_http.egg-info/dependency_links.txt +0 -0
  39. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/python_openevse_http.egg-info/not-zip-safe +0 -0
  40. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/python_openevse_http.egg-info/requires.txt +0 -0
  41. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/python_openevse_http.egg-info/top_level.txt +0 -0
  42. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/requirements.txt +0 -0
  43. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/setup.cfg +0 -0
  44. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/setup.py +0 -0
  45. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/__init__.py +0 -0
  46. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/common.py +0 -0
  47. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/conftest.py +0 -0
  48. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/fixtures/github_v2.json +0 -0
  49. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/fixtures/github_v4.json +0 -0
  50. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/fixtures/v2_json/config.json +0 -0
  51. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/fixtures/v2_json/status.json +0 -0
  52. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/fixtures/v4_json/config-broken-semver.json +0 -0
  53. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/fixtures/v4_json/config-broken.json +0 -0
  54. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/fixtures/v4_json/config-dev.json +0 -0
  55. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/fixtures/v4_json/config-extra-version.json +0 -0
  56. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/fixtures/v4_json/config-new.json +0 -0
  57. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/fixtures/v4_json/config.json +0 -0
  58. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/fixtures/v4_json/schedule.json +0 -0
  59. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/fixtures/v4_json/status-broken.json +0 -0
  60. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/fixtures/v4_json/status-new.json +0 -0
  61. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/fixtures/v4_json/status.json +0 -0
  62. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/fixtures/websocket.json +0 -0
  63. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/test_commands.py +0 -0
  64. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/test_external_session.py +0 -0
  65. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/test_main_edge_cases.py +0 -0
  66. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/test_managers.py +0 -0
  67. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/test_mixins.py +0 -0
  68. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/test_sensors.py +0 -0
  69. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/test_shaper.py +0 -0
  70. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tests/test_websocket.py +0 -0
  71. {python_openevse_http-1.0.0 → python_openevse_http-1.0.2}/tox.ini +0 -0
@@ -19,7 +19,7 @@ jobs:
19
19
  with:
20
20
  egress-policy: audit
21
21
 
22
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
22
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
23
23
 
24
24
  - name: Link Checker
25
25
  uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2
@@ -25,13 +25,13 @@ jobs:
25
25
  with:
26
26
  egress-policy: audit
27
27
 
28
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
28
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
29
29
  with:
30
30
  ref: ${{ inputs.tag || github.ref }}
31
31
  fetch-depth: 0
32
32
 
33
33
  - name: Install uv
34
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
34
+ uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
35
35
  with:
36
36
  enable-cache: true
37
37
  version: "0.10.9"
@@ -22,7 +22,7 @@ jobs:
22
22
  with:
23
23
  egress-policy: audit
24
24
 
25
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
25
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
48
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
77
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
78
78
  with:
79
79
  fetch-depth: 2
80
80
  - name: Download coverage data
@@ -82,6 +82,6 @@ jobs:
82
82
  with:
83
83
  name: coverage-data
84
84
  - name: Upload coverage report
85
- uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6
85
+ uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6
86
86
  with:
87
87
  token: ${{ secrets.CODECOV_TOKEN }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_openevse_http
3
- Version: 1.0.0
3
+ Version: 1.0.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
@@ -50,13 +50,13 @@ class OpenEVSE(CommandsMixin, ManagersMixin, SensorsMixin, PropertiesMixin):
50
50
  def __init__(
51
51
  self,
52
52
  host: str,
53
- user: str = "",
54
- pwd: str = "",
53
+ user: str | None = None,
54
+ pwd: str | None = None,
55
55
  session: aiohttp.ClientSession | None = None,
56
56
  ) -> None:
57
57
  """Connect to an OpenEVSE charger equipped with wifi or ethernet."""
58
- self._user = user
59
- self._pwd = pwd
58
+ self._user = user or ""
59
+ self._pwd = pwd or ""
60
60
  self.url = f"http://{host}/"
61
61
  self._status: dict[str, Any] = {}
62
62
  self._config: dict[str, Any] = {}
@@ -84,9 +84,12 @@ class PropertiesMixin:
84
84
  return bool(self._config.get("relayt", False))
85
85
 
86
86
  @property
87
- def service_level(self) -> int | str | None:
88
- """Return the service level (1, 2, or 'A')."""
89
- return self._config.get("service")
87
+ def service_level(self) -> str | None:
88
+ """Return the service level ('1', '2', or 'A')."""
89
+ value = self._config.get("service")
90
+ if value is None:
91
+ return value
92
+ return str(value)
90
93
 
91
94
  @property
92
95
  def openevse_firmware(self) -> str | None:
@@ -0,0 +1,55 @@
1
+ """Utility functions for python-openevse-http."""
2
+
3
+ import logging
4
+ import re
5
+
6
+ from awesomeversion import AwesomeVersion
7
+
8
+ _LOGGER = logging.getLogger(__name__)
9
+
10
+
11
+ def normalize_version(version: str) -> str:
12
+ """Normalize the version string to strip 'dev' tag."""
13
+ if "dev" in version:
14
+ _LOGGER.debug("Stripping 'dev' from version.")
15
+ value = version.split(".")
16
+ return ".".join(value[0:3])
17
+ return version
18
+
19
+
20
+ def get_awesome_version(version: str) -> AwesomeVersion:
21
+ """Parse and normalize the version string, returning an AwesomeVersion.
22
+
23
+ Development and custom firmware versions (e.g. branch builds ending in a commit hash,
24
+ or standalone 'main'/'master' builds) are normalized to 'dev' to bypass feature flags
25
+ and version-checks.
26
+
27
+ Non-dev version strings are normalized by extracting the first stable 'X.Y.Z' match,
28
+ meaning pre-release, build, or other trailing tags (such as '_rc1' or '_alpha') are
29
+ removed and not preserved in the returned version.
30
+ """
31
+ # Match non-numeric git branch names (e.g. 'main', 'master', 'develop', or 'feature-x_abc123456')
32
+ # We use custom word boundary checks to avoid false positives like 'domain' matching 'main'
33
+ # or 'webmaster' matching 'master'.
34
+ is_dev = False
35
+ if re.search(
36
+ r"(?:^|[^a-zA-Z0-9])(master|main)(?:[^a-zA-Z0-9]|$)", version, re.IGNORECASE
37
+ ):
38
+ is_dev = True
39
+ elif re.search(r"_[a-fA-F0-9]{7,40}$", version):
40
+ is_dev = True
41
+ elif re.search(
42
+ r"(?:^|[^a-zA-Z0-9])(dev|feature|fix|main|master)(?:[^a-zA-Z0-9].*?)?_[a-fA-F0-9]{6}$",
43
+ version,
44
+ re.IGNORECASE,
45
+ ):
46
+ is_dev = True
47
+
48
+ if is_dev:
49
+ version = "dev"
50
+ value = normalize_version(version)
51
+ if "dev" not in version:
52
+ firmware_search = re.search(r"\d+\.\d+\.\d+", value)
53
+ if firmware_search:
54
+ value = firmware_search.group(0)
55
+ return AwesomeVersion(value)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_openevse_http
3
- Version: 1.0.0
3
+ Version: 1.0.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
@@ -1,4 +1,4 @@
1
1
  -r requirements.txt
2
- prek==0.4.3
3
- ruff==0.15.15
2
+ prek==0.4.4
3
+ ruff==0.15.16
4
4
  mypy==2.1.0
@@ -1,10 +1,10 @@
1
1
  -r requirements.txt
2
- prek==0.4.3
2
+ prek==0.4.4
3
3
  pytest==9.0.3
4
4
  pytest-cov==7.1.0
5
5
  pytest-timeout==2.4.0
6
6
  pytest-asyncio
7
7
  requests_mock
8
8
  aiohttp
9
- tox==4.55.0
9
+ tox==4.55.1
10
10
  freezegun
@@ -3,7 +3,7 @@
3
3
  "protocol": "-",
4
4
  "espflash": 4194304,
5
5
  "wifi_serial": "1234567890AB",
6
- "version": "random_a4f11e",
6
+ "version": "random_g4f11e",
7
7
  "diodet": 0,
8
8
  "gfcit": 0,
9
9
  "groundt": 0,
@@ -424,7 +424,8 @@ async def test_firmware_check(
424
424
  assert firmware["latest_version"] == "4.1.4"
425
425
 
426
426
  await test_charger_unknown_semver.update()
427
- assert test_charger_unknown_semver.wifi_firmware == "random_a4f11e"
427
+ # Changed from 'random_a4f11e' to 'random_g4f11e' to avoid false positive matches on the 6-character hex hash regex
428
+ assert test_charger_unknown_semver.wifi_firmware == "random_g4f11e"
428
429
  mock_aioclient.get(
429
430
  TEST_URL_GITHUB_v4,
430
431
  status=200,
@@ -432,7 +433,7 @@ async def test_firmware_check(
432
433
  )
433
434
  with caplog.at_level(logging.DEBUG):
434
435
  firmware = await test_charger_unknown_semver.firmware_check()
435
- assert "Using version: random_a4f11e" in caplog.text
436
+ assert "Using version: random_g4f11e" in caplog.text
436
437
  assert "Non-semver firmware version detected" in caplog.text
437
438
  assert firmware is None
438
439
 
@@ -634,6 +635,59 @@ async def test_version_check_limit():
634
635
  assert charger.version_check("2.0.0") is True
635
636
 
636
637
 
638
+ async def test_version_check_dev_branches():
639
+ """Test _version_check with dev branches.
640
+
641
+ Test that dev branch version strings bypass version checks and are correctly
642
+ treated as development builds. Also tests false-positive avoidance and
643
+ pre-release handling.
644
+ """
645
+ charger = OpenEVSE(SERVER_URL, session=MagicMock())
646
+
647
+ # Standalone dev branches - treated as dev, returns True
648
+ charger._config = {"version": "main"}
649
+ assert charger._version_check("2.0.0") is True
650
+
651
+ charger._config = {"version": "master"}
652
+ assert charger._version_check("2.0.0") is True
653
+
654
+ # 'main' branch with hash - treated as dev, returns True
655
+ charger._config = {"version": "main_abc1234"}
656
+ assert charger._version_check("2.0.0") is True
657
+
658
+ # 'master' branch with hash - treated as dev, returns True
659
+ charger._config = {"version": "master_abc1234"}
660
+ assert charger._version_check("2.0.0") is True
661
+
662
+ # Custom branch with 7-char hash - treated as dev, returns True
663
+ charger._config = {"version": "feature-ui_2b4ad2c"}
664
+ assert charger._version_check("2.0.0") is True
665
+
666
+ # Custom branch with 6-char hash - treated as dev, returns True
667
+ charger._config = {"version": "feature-ui_2b4ad2"}
668
+ assert charger._version_check("2.0.0") is True
669
+
670
+ # Case-insensitive hex hash matching (uppercase) - treated as dev, returns True
671
+ charger._config = {"version": "feature_1A2B3C"}
672
+ assert charger._version_check("2.0.0") is True
673
+
674
+ # False positives containing 'main' or 'master' but not at word boundaries,
675
+ # or ending in 6-char hashes without dev keywords — should fail version check
676
+ charger._config = {"version": "domain_abc123"}
677
+ assert charger._version_check("2.0.0") is False
678
+
679
+ charger._config = {"version": "webmaster_def456"}
680
+ assert charger._version_check("2.0.0") is False
681
+
682
+ # Pre-release (rc) version — should fail when checking against a newer target
683
+ charger._config = {"version": "4.1.2_rc1"}
684
+ assert charger._version_check("5.0.0") is False
685
+
686
+ # Pre-release (alpha) version — should also fail when checking against a newer target
687
+ charger._config = {"version": "4.1.0_alpha"}
688
+ assert charger._version_check("5.0.0") is False
689
+
690
+
637
691
  # ── websocket lifecycle ──────────────────────────────────────────────
638
692
 
639
693
 
@@ -56,8 +56,8 @@ SERVER_URL = "openevse.test.tld"
56
56
  ("test_charger_v2", "ammeter_offset", 0),
57
57
  ("test_charger", "ammeter_scale_factor", 220),
58
58
  ("test_charger_v2", "ammeter_scale_factor", 220),
59
- ("test_charger", "service_level", 2),
60
- ("test_charger_v2", "service_level", 2),
59
+ ("test_charger", "service_level", "2"),
60
+ ("test_charger_v2", "service_level", "2"),
61
61
  # safety checks
62
62
  ("test_charger", "temp_check_enabled", False),
63
63
  ("test_charger_v2", "temp_check_enabled", False),
@@ -1,29 +0,0 @@
1
- """Utility functions for python-openevse-http."""
2
-
3
- import logging
4
- import re
5
-
6
- from awesomeversion import AwesomeVersion
7
-
8
- _LOGGER = logging.getLogger(__name__)
9
-
10
-
11
- def normalize_version(version: str) -> str:
12
- """Normalize the version string to strip 'dev' tag."""
13
- if "dev" in version:
14
- _LOGGER.debug("Stripping 'dev' from version.")
15
- value = version.split(".")
16
- return ".".join(value[0:3])
17
- return version
18
-
19
-
20
- def get_awesome_version(version: str) -> AwesomeVersion:
21
- """Parse and normalize the version string, returning an AwesomeVersion."""
22
- if "master" in version:
23
- version = "dev"
24
- value = normalize_version(version)
25
- if "dev" not in version:
26
- firmware_search = re.search(r"\d+\.\d+\.\d+", value)
27
- if firmware_search:
28
- value = firmware_search.group(0)
29
- return AwesomeVersion(value)