From f26ee3e79689f07135792a2d8705478a565af106 Mon Sep 17 00:00:00 2001 From: "firstof9@gmail.com" Date: Wed, 10 Jun 2026 08:51:36 -0700 Subject: [PATCH 1/5] fix(utils): support dev and custom firmware version strings --- openevsehttp/utils.py | 6 +++++- tests/test_client.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/openevsehttp/utils.py b/openevsehttp/utils.py index 8c8e30d4..ab34ae89 100644 --- a/openevsehttp/utils.py +++ b/openevsehttp/utils.py @@ -19,7 +19,11 @@ def normalize_version(version: str) -> str: def get_awesome_version(version: str) -> AwesomeVersion: """Parse and normalize the version string, returning an AwesomeVersion.""" - if "master" in version: + if ( + "master" in version + or "main" in version + or (len(version) > 7 and re.search(r"_[a-f0-9]{7,40}$", version)) + ): version = "dev" value = normalize_version(version) if "dev" not in version: diff --git a/tests/test_client.py b/tests/test_client.py index 28c6394a..e6ea321a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -634,6 +634,23 @@ async def test_version_check_limit(): assert charger.version_check("2.0.0") is True +async def test_version_check_dev_branches(): + """Test _version_check with dev branches like 'main' and custom branches with hashes.""" + charger = OpenEVSE(SERVER_URL, session=MagicMock()) + + # 'main' branch + charger._config = {"version": "main_abc1234"} + assert charger._version_check("2.0.0") is True + + # Custom branch with 7-char hash + charger._config = {"version": "feature-ui_2b4ad2c"} + assert charger._version_check("2.0.0") is True + + # Non-dev with underscores (should fail) + charger._config = {"version": "4.1.2_rc1"} + assert charger._version_check("5.0.0") is False + + # ── websocket lifecycle ────────────────────────────────────────────── From 41ac2db2dfe01bc37a6f48fe87ad081d21adc24b Mon Sep 17 00:00:00 2001 From: "firstof9@gmail.com" Date: Wed, 10 Jun 2026 09:39:10 -0700 Subject: [PATCH 2/5] test/fix: address code review comments on PR 613 --- openevsehttp/utils.py | 3 ++- .../v4_json/config-unknown-semver.json | 2 +- tests/test_client.py | 26 ++++++++++++++----- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/openevsehttp/utils.py b/openevsehttp/utils.py index ab34ae89..55499ca1 100644 --- a/openevsehttp/utils.py +++ b/openevsehttp/utils.py @@ -19,10 +19,11 @@ def normalize_version(version: str) -> str: def get_awesome_version(version: str) -> AwesomeVersion: """Parse and normalize the version string, returning an AwesomeVersion.""" + # Match non-numeric git branch names (e.g. 'main', 'master', 'develop', or 'feature-x_abc123456') if ( "master" in version or "main" in version - or (len(version) > 7 and re.search(r"_[a-f0-9]{7,40}$", version)) + or re.search(r"_[a-f0-9]{6,40}$", version) ): version = "dev" value = normalize_version(version) diff --git a/tests/fixtures/v4_json/config-unknown-semver.json b/tests/fixtures/v4_json/config-unknown-semver.json index 60d1aff5..f38d0984 100644 --- a/tests/fixtures/v4_json/config-unknown-semver.json +++ b/tests/fixtures/v4_json/config-unknown-semver.json @@ -3,7 +3,7 @@ "protocol": "-", "espflash": 4194304, "wifi_serial": "1234567890AB", - "version": "random_a4f11e", + "version": "random_g4f11e", "diodet": 0, "gfcit": 0, "groundt": 0, diff --git a/tests/test_client.py b/tests/test_client.py index e6ea321a..496f7b92 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -424,7 +424,7 @@ async def test_firmware_check( assert firmware["latest_version"] == "4.1.4" await test_charger_unknown_semver.update() - assert test_charger_unknown_semver.wifi_firmware == "random_a4f11e" + assert test_charger_unknown_semver.wifi_firmware == "random_g4f11e" mock_aioclient.get( TEST_URL_GITHUB_v4, status=200, @@ -432,7 +432,7 @@ async def test_firmware_check( ) with caplog.at_level(logging.DEBUG): firmware = await test_charger_unknown_semver.firmware_check() - assert "Using version: random_a4f11e" in caplog.text + assert "Using version: random_g4f11e" in caplog.text assert "Non-semver firmware version detected" in caplog.text assert firmware is None @@ -635,21 +635,35 @@ async def test_version_check_limit(): async def test_version_check_dev_branches(): - """Test _version_check with dev branches like 'main' and custom branches with hashes.""" + """Test _version_check with dev branches, custom branches with hashes, and pre-releases. + + Dev branches (containing 'main', 'master', or ending in a hex commit hash) + should bypass version checks and return True. Non-dev pre-releases (like rc + or alpha) should be parsed normally and fail when compared against a newer + target version. + """ charger = OpenEVSE(SERVER_URL, session=MagicMock()) - # 'main' branch + # 'main' branch - treated as dev, returns True charger._config = {"version": "main_abc1234"} assert charger._version_check("2.0.0") is True - # Custom branch with 7-char hash + # Custom branch with 7-char hash - treated as dev, returns True charger._config = {"version": "feature-ui_2b4ad2c"} assert charger._version_check("2.0.0") is True - # Non-dev with underscores (should fail) + # Custom branch with 6-char hash - treated as dev, returns True + charger._config = {"version": "feature-ui_2b4ad2"} + assert charger._version_check("2.0.0") is True + + # Pre-release (rc) version — should fail when checking against a newer target charger._config = {"version": "4.1.2_rc1"} assert charger._version_check("5.0.0") is False + # Pre-release (alpha) version — should also fail when checking against a newer target + charger._config = {"version": "4.1.0_alpha"} + assert charger._version_check("5.0.0") is False + # ── websocket lifecycle ────────────────────────────────────────────── From add4d1f11c5a5f2a84e1d19a0d02bb2e9de4197d Mon Sep 17 00:00:00 2001 From: "firstof9@gmail.com" Date: Wed, 10 Jun 2026 10:13:59 -0700 Subject: [PATCH 3/5] test/fix: add case-insensitivity, word boundaries, and expanded tests for PR 613 comments --- openevsehttp/utils.py | 20 ++++++++++++++++---- tests/test_client.py | 25 ++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/openevsehttp/utils.py b/openevsehttp/utils.py index 55499ca1..490179bc 100644 --- a/openevsehttp/utils.py +++ b/openevsehttp/utils.py @@ -20,11 +20,23 @@ def normalize_version(version: str) -> str: def get_awesome_version(version: str) -> AwesomeVersion: """Parse and normalize the version string, returning an AwesomeVersion.""" # Match non-numeric git branch names (e.g. 'main', 'master', 'develop', or 'feature-x_abc123456') - if ( - "master" in version - or "main" in version - or re.search(r"_[a-f0-9]{6,40}$", version) + # We use custom word boundary checks to avoid false positives like 'domain' matching 'main' + # or 'webmaster' matching 'master'. + is_dev = False + if re.search( + r"(?:^|[^a-zA-Z0-9])(master|main)(?:[^a-zA-Z0-9]|$)", version, re.IGNORECASE ): + is_dev = True + elif re.search(r"_[a-fA-F0-9]{7,40}$", version): + is_dev = True + elif re.search( + r"(?:^|[^a-zA-Z0-9])(dev|feature|fix|main|master)(?:[^a-zA-Z0-9].*?)?_[a-fA-F0-9]{6}$", + version, + re.IGNORECASE, + ): + is_dev = True + + if is_dev: version = "dev" value = normalize_version(version) if "dev" not in version: diff --git a/tests/test_client.py b/tests/test_client.py index 496f7b92..ec379c92 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -644,10 +644,21 @@ async def test_version_check_dev_branches(): """ charger = OpenEVSE(SERVER_URL, session=MagicMock()) - # 'main' branch - treated as dev, returns True + # Standalone dev branches - treated as dev, returns True + charger._config = {"version": "main"} + assert charger._version_check("2.0.0") is True + + charger._config = {"version": "master"} + assert charger._version_check("2.0.0") is True + + # 'main' branch with hash - treated as dev, returns True charger._config = {"version": "main_abc1234"} assert charger._version_check("2.0.0") is True + # 'master' branch with hash - treated as dev, returns True + charger._config = {"version": "master_abc1234"} + assert charger._version_check("2.0.0") is True + # Custom branch with 7-char hash - treated as dev, returns True charger._config = {"version": "feature-ui_2b4ad2c"} assert charger._version_check("2.0.0") is True @@ -656,6 +667,18 @@ async def test_version_check_dev_branches(): charger._config = {"version": "feature-ui_2b4ad2"} assert charger._version_check("2.0.0") is True + # Case-insensitive hex hash matching (uppercase) - treated as dev, returns True + charger._config = {"version": "feature_1A2B3C"} + assert charger._version_check("2.0.0") is True + + # False positives containing 'main' or 'master' but not at word boundaries, + # or ending in 6-char hashes without dev keywords — should fail version check + charger._config = {"version": "domain_abc123"} + assert charger._version_check("2.0.0") is False + + charger._config = {"version": "webmaster_def456"} + assert charger._version_check("2.0.0") is False + # Pre-release (rc) version — should fail when checking against a newer target charger._config = {"version": "4.1.2_rc1"} assert charger._version_check("5.0.0") is False From 3c4c8015a15a6bb5c0be299b77b4163391bbfa03 Mon Sep 17 00:00:00 2001 From: "firstof9@gmail.com" Date: Wed, 10 Jun 2026 11:28:38 -0700 Subject: [PATCH 4/5] doc: add docstrings and comments addressing PR feedback --- openevsehttp/utils.py | 7 ++++++- tests/test_client.py | 10 +++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/openevsehttp/utils.py b/openevsehttp/utils.py index 490179bc..40153466 100644 --- a/openevsehttp/utils.py +++ b/openevsehttp/utils.py @@ -18,7 +18,12 @@ def normalize_version(version: str) -> str: def get_awesome_version(version: str) -> AwesomeVersion: - """Parse and normalize the version string, returning an AwesomeVersion.""" + """Parse and normalize the version string, returning an AwesomeVersion. + + Development and custom firmware versions (e.g. branch builds ending in a commit hash, + or standalone 'main'/'master' builds) are normalized to 'dev' to bypass feature flags + and version-checks. Normal release and pre-release versions are parsed as standard semver. + """ # Match non-numeric git branch names (e.g. 'main', 'master', 'develop', or 'feature-x_abc123456') # We use custom word boundary checks to avoid false positives like 'domain' matching 'main' # or 'webmaster' matching 'master'. diff --git a/tests/test_client.py b/tests/test_client.py index ec379c92..07b95e72 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -424,6 +424,7 @@ async def test_firmware_check( assert firmware["latest_version"] == "4.1.4" await test_charger_unknown_semver.update() + # Changed from 'random_a4f11e' to 'random_g4f11e' to avoid false positive matches on the 6-character hex hash regex assert test_charger_unknown_semver.wifi_firmware == "random_g4f11e" mock_aioclient.get( TEST_URL_GITHUB_v4, @@ -635,12 +636,11 @@ async def test_version_check_limit(): async def test_version_check_dev_branches(): - """Test _version_check with dev branches, custom branches with hashes, and pre-releases. + """Test _version_check with dev branches. - Dev branches (containing 'main', 'master', or ending in a hex commit hash) - should bypass version checks and return True. Non-dev pre-releases (like rc - or alpha) should be parsed normally and fail when compared against a newer - target version. + Test that dev branch version strings bypass version checks and are correctly + treated as development builds. Also tests false-positive avoidance and + pre-release handling. """ charger = OpenEVSE(SERVER_URL, session=MagicMock()) From 64cea99d501189642bb751015a131b578706b7f3 Mon Sep 17 00:00:00 2001 From: "firstof9@gmail.com" Date: Wed, 10 Jun 2026 12:21:18 -0700 Subject: [PATCH 5/5] doc: clarify non-dev pre-release stripping behavior in docstring --- openevsehttp/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openevsehttp/utils.py b/openevsehttp/utils.py index 40153466..d06f5217 100644 --- a/openevsehttp/utils.py +++ b/openevsehttp/utils.py @@ -22,7 +22,11 @@ def get_awesome_version(version: str) -> AwesomeVersion: Development and custom firmware versions (e.g. branch builds ending in a commit hash, or standalone 'main'/'master' builds) are normalized to 'dev' to bypass feature flags - and version-checks. Normal release and pre-release versions are parsed as standard semver. + and version-checks. + + Non-dev version strings are normalized by extracting the first stable 'X.Y.Z' match, + meaning pre-release, build, or other trailing tags (such as '_rc1' or '_alpha') are + removed and not preserved in the returned version. """ # Match non-numeric git branch names (e.g. 'main', 'master', 'develop', or 'feature-x_abc123456') # We use custom word boundary checks to avoid false positives like 'domain' matching 'main'