Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions openevsehttp/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,34 @@ 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:
"""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.

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'
# 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:
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/v4_json/config-unknown-semver.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"protocol": "-",
"espflash": 4194304,
"wifi_serial": "1234567890AB",
"version": "random_a4f11e",
"version": "random_g4f11e",
"diodet": 0,
"gfcit": 0,
"groundt": 0,
Expand Down
58 changes: 56 additions & 2 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,15 +424,16 @@ 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"
# 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,
status=200,
body=load_fixture("github_v4.json"),
)
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

Expand Down Expand Up @@ -634,6 +635,59 @@ 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.

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())

# 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

# 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

# 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

# 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 ──────────────────────────────────────────────


Expand Down
Loading