Skip to content

Commit 4d57220

Browse files
authored
fix: handle non-semver version strings better (#227)
* fix: handle non-semver version strings better * missed variable and formatting * udpate tests * update test * update tests * formatting * linting * more tests * formatting
1 parent c0b630a commit 4d57220

File tree

6 files changed

+253
-17
lines changed

6 files changed

+253
-17
lines changed

openevsehttp/__main__.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import aiohttp # type: ignore
1111
from aiohttp.client_exceptions import ContentTypeError, ServerTimeoutError
1212
from awesomeversion import AwesomeVersion
13+
from awesomeversion.exceptions import AwesomeVersionCompareException
1314

1415
from .const import MAX_AMPS, MIN_AMPS
1516
from .exceptions import (
@@ -489,15 +490,22 @@ async def firmware_check(self) -> dict | None:
489490
_LOGGER.debug("Stripping 'dev' from version.")
490491
value = value.split(".")
491492
value = ".".join(value[0:3])
492-
_LOGGER.debug("Using version: %s", value)
493-
current = AwesomeVersion(value)
493+
elif "master" in self._config["version"]:
494+
value = "dev"
494495
else:
495-
current = AwesomeVersion(self._config["version"])
496+
value = self._config["version"]
496497

497-
if current >= cutoff:
498-
url = f"{base_url}ESP32_WiFi_V4.x/releases/latest"
499-
else:
500-
url = f"{base_url}ESP8266_WiFi_v2.x/releases/latest"
498+
_LOGGER.debug("Using version: %s", value)
499+
current = AwesomeVersion(value)
500+
501+
try:
502+
if current >= cutoff:
503+
url = f"{base_url}ESP32_WiFi_V4.x/releases/latest"
504+
else:
505+
url = f"{base_url}ESP8266_WiFi_v2.x/releases/latest"
506+
except AwesomeVersionCompareException:
507+
_LOGGER.warning("Non-semver firmware version detected.")
508+
return None
501509

502510
try:
503511
async with aiohttp.ClientSession() as session:
@@ -551,16 +559,26 @@ def _version_check(self, min_version: str, max_version: str = "") -> bool:
551559
_LOGGER.debug("Stripping 'dev' from version.")
552560
value = value.split(".")
553561
value = ".".join(value[0:3])
554-
current = AwesomeVersion(value)
562+
elif "master" in self._config["version"]:
563+
value = "dev"
555564
else:
556-
current = AwesomeVersion(self._config["version"])
565+
value = self._config["version"]
566+
567+
current = AwesomeVersion(value)
557568

558569
if limit:
559-
if cutoff <= current <= limit:
560-
return True
570+
try:
571+
if cutoff <= current <= limit:
572+
return True
573+
except AwesomeVersionCompareException:
574+
_LOGGER.debug("Non-semver firmware version detected.")
561575
return False
562-
if current >= cutoff:
563-
return True
576+
577+
try:
578+
if current >= cutoff:
579+
return True
580+
except AwesomeVersionCompareException:
581+
_LOGGER.debug("Non-semver firmware version detected.")
564582
return False
565583

566584
@property

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
PROJECT_DIR = Path(__file__).parent.resolve()
77
README_FILE = PROJECT_DIR / "README.md"
8-
VERSION = "0.1.50"
8+
VERSION = "0.1.51"
99

1010
setup(
1111
name="python-openevse-http",

tests/conftest.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,38 @@ def test_charger_broken(mock_aioclient):
107107
return main.OpenEVSE(TEST_TLD)
108108

109109

110+
@pytest.fixture(name="test_charger_broken_semver")
111+
def test_charger_broken_semver(mock_aioclient):
112+
"""Load the charger data."""
113+
mock_aioclient.get(
114+
TEST_URL_STATUS,
115+
status=200,
116+
body=load_fixture("v4_json/status.json"),
117+
)
118+
mock_aioclient.get(
119+
TEST_URL_CONFIG,
120+
status=200,
121+
body=load_fixture("v4_json/config-broken-semver.json"),
122+
)
123+
return main.OpenEVSE(TEST_TLD)
124+
125+
126+
@pytest.fixture(name="test_charger_unknown_semver")
127+
def test_charger_unknown_semver(mock_aioclient):
128+
"""Load the charger data."""
129+
mock_aioclient.get(
130+
TEST_URL_STATUS,
131+
status=200,
132+
body=load_fixture("v4_json/status.json"),
133+
)
134+
mock_aioclient.get(
135+
TEST_URL_CONFIG,
136+
status=200,
137+
body=load_fixture("v4_json/config-unknown-semver.json"),
138+
)
139+
return main.OpenEVSE(TEST_TLD)
140+
141+
110142
@pytest.fixture(name="test_charger_v2")
111143
def test_charger_v2(mock_aioclient):
112144
"""Load the charger data."""
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"firmware": "7.1.3",
3+
"protocol": "-",
4+
"espflash": 4194304,
5+
"wifi_serial": "1234567890AB",
6+
"version": "master_abcd123",
7+
"diodet": 0,
8+
"gfcit": 0,
9+
"groundt": 0,
10+
"relayt": 0,
11+
"ventt": 0,
12+
"tempt": 0,
13+
"service": 2,
14+
"scale": 220,
15+
"offset": 0,
16+
"max_current_soft": 48,
17+
"min_current_hard": 6,
18+
"max_current_hard": 48,
19+
"mqtt_supported_protocols": [
20+
"mqtt",
21+
"mqtts"
22+
],
23+
"http_supported_protocols": [
24+
"http",
25+
"https"
26+
],
27+
"ssid": "Datanode-IoT",
28+
"pass": "_DUMMY_PASSWORD",
29+
"www_username": "",
30+
"www_password": "",
31+
"hostname": "openevse-7b2c",
32+
"sntp_hostname": "0.us.pool.ntp.org",
33+
"time_zone": "America/Phoenix|MST7",
34+
"emoncms_server": "https://emoncms.collective.lan/",
35+
"emoncms_node": "openevse",
36+
"emoncms_apikey": "_DUMMY_PASSWORD",
37+
"emoncms_fingerprint": "",
38+
"mqtt_server": "192.168.1.198",
39+
"mqtt_port": 1883,
40+
"mqtt_topic": "openevse",
41+
"mqtt_user": "devices",
42+
"mqtt_pass": "_DUMMY_PASSWORD",
43+
"mqtt_solar": "",
44+
"mqtt_grid_ie": "home-assistant/power/watts",
45+
"mqtt_vrms": "home-assistant/solar/watts",
46+
"mqtt_announce_topic": "openevse/announce/7b2c",
47+
"ohm": "",
48+
"divert_PV_ratio": 1.1,
49+
"divert_attack_smoothing_factor": 0.4,
50+
"divert_decay_smoothing_factor": 0.05,
51+
"divert_min_charge_time": 600,
52+
"tesla_username": "",
53+
"tesla_password": "",
54+
"tesla_vehidx": -1,
55+
"led_brightness": 128,
56+
"flags": 522,
57+
"emoncms_enabled": false,
58+
"mqtt_enabled": true,
59+
"mqtt_reject_unauthorized": true,
60+
"ohm_enabled": false,
61+
"sntp_enabled": true,
62+
"tesla_enabled": false,
63+
"pause_uses_disabled": false,
64+
"mqtt_protocol": "mqtt",
65+
"charge_mode": "fast"
66+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"firmware": "7.1.3",
3+
"protocol": "-",
4+
"espflash": 4194304,
5+
"wifi_serial": "1234567890AB",
6+
"version": "random_a4f11e",
7+
"diodet": 0,
8+
"gfcit": 0,
9+
"groundt": 0,
10+
"relayt": 0,
11+
"ventt": 0,
12+
"tempt": 0,
13+
"service": 2,
14+
"scale": 220,
15+
"offset": 0,
16+
"max_current_soft": 48,
17+
"min_current_hard": 6,
18+
"max_current_hard": 48,
19+
"mqtt_supported_protocols": [
20+
"mqtt",
21+
"mqtts"
22+
],
23+
"http_supported_protocols": [
24+
"http",
25+
"https"
26+
],
27+
"ssid": "Datanode-IoT",
28+
"pass": "_DUMMY_PASSWORD",
29+
"www_username": "",
30+
"www_password": "",
31+
"hostname": "openevse-7b2c",
32+
"sntp_hostname": "0.us.pool.ntp.org",
33+
"time_zone": "America/Phoenix|MST7",
34+
"emoncms_server": "https://emoncms.collective.lan/",
35+
"emoncms_node": "openevse",
36+
"emoncms_apikey": "_DUMMY_PASSWORD",
37+
"emoncms_fingerprint": "",
38+
"mqtt_server": "192.168.1.198",
39+
"mqtt_port": 1883,
40+
"mqtt_topic": "openevse",
41+
"mqtt_user": "devices",
42+
"mqtt_pass": "_DUMMY_PASSWORD",
43+
"mqtt_solar": "",
44+
"mqtt_grid_ie": "home-assistant/power/watts",
45+
"mqtt_vrms": "home-assistant/solar/watts",
46+
"mqtt_announce_topic": "openevse/announce/7b2c",
47+
"ohm": "",
48+
"divert_PV_ratio": 1.1,
49+
"divert_attack_smoothing_factor": 0.4,
50+
"divert_decay_smoothing_factor": 0.05,
51+
"divert_min_charge_time": 600,
52+
"tesla_username": "",
53+
"tesla_password": "",
54+
"tesla_vehidx": -1,
55+
"led_brightness": 128,
56+
"flags": 522,
57+
"emoncms_enabled": false,
58+
"mqtt_enabled": true,
59+
"mqtt_reject_unauthorized": true,
60+
"ohm_enabled": false,
61+
"sntp_enabled": true,
62+
"tesla_enabled": false,
63+
"pause_uses_disabled": false,
64+
"mqtt_protocol": "mqtt",
65+
"charge_mode": "fast"
66+
}

tests/test_main.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import logging
88
from unittest import mock
99

10+
from awesomeversion.exceptions import AwesomeVersionCompareException
11+
1012
import pytest
1113
from aiohttp.client_exceptions import ContentTypeError, ServerTimeoutError
1214

@@ -230,6 +232,7 @@ async def test_get_service_level(fixture, expected, request):
230232
("test_charger", "4.1.2"),
231233
("test_charger_v2", "2.9.1"),
232234
("test_charger_dev", "4.1.5"),
235+
("test_charger_broken_semver", "master_abcd123"),
233236
],
234237
)
235238
async def test_get_wifi_firmware(fixture, expected, request):
@@ -641,7 +644,12 @@ async def test_get_manual_override(fixture, expected, request):
641644

642645

643646
async def test_toggle_override(
644-
test_charger, test_charger_dev, test_charger_new, mock_aioclient, caplog
647+
test_charger,
648+
test_charger_dev,
649+
test_charger_new,
650+
test_charger_unknown_semver,
651+
mock_aioclient,
652+
caplog,
645653
):
646654
"""Test v4 Status reply."""
647655
await test_charger.update()
@@ -848,7 +856,12 @@ async def test_get_charging_power(fixture, expected, request):
848856

849857

850858
async def test_set_divertmode(
851-
test_charger_new, test_charger_v2, test_charger_broken, mock_aioclient, caplog
859+
test_charger_new,
860+
test_charger_v2,
861+
test_charger_broken,
862+
test_charger_unknown_semver,
863+
mock_aioclient,
864+
caplog,
852865
):
853866
"""Test v4 set divert mode."""
854867
await test_charger_new.update()
@@ -896,6 +909,17 @@ async def test_set_divertmode(
896909
with pytest.raises(UnsupportedFeature):
897910
await test_charger_broken.divert_mode()
898911

912+
mock_aioclient.post(
913+
TEST_URL_CONFIG,
914+
status=200,
915+
body=value,
916+
)
917+
await test_charger_unknown_semver.update()
918+
with pytest.raises(UnsupportedFeature):
919+
with caplog.at_level(logging.DEBUG):
920+
await test_charger_unknown_semver.divert_mode()
921+
assert "Non-semver firmware version detected." in caplog.text
922+
899923

900924
async def test_test_and_get(test_charger, test_charger_v2, mock_aioclient, caplog):
901925
"""Test v4 Status reply"""
@@ -931,6 +955,8 @@ async def test_firmware_check(
931955
test_charger_dev,
932956
test_charger_v2,
933957
test_charger_broken,
958+
test_charger_broken_semver,
959+
test_charger_unknown_semver,
934960
mock_aioclient,
935961
caplog,
936962
):
@@ -998,6 +1024,28 @@ async def test_firmware_check(
9981024
assert "Unable to find firmware version." in caplog.text
9991025
assert firmware is None
10001026

1027+
await test_charger_broken_semver.update()
1028+
mock_aioclient.get(
1029+
TEST_URL_GITHUB_v4,
1030+
status=200,
1031+
body=load_fixture("github_v4.json"),
1032+
)
1033+
firmware = await test_charger_broken_semver.firmware_check()
1034+
assert firmware["latest_version"] == "4.1.4"
1035+
1036+
await test_charger_unknown_semver.update()
1037+
assert test_charger_unknown_semver.wifi_firmware == "random_a4f11e"
1038+
mock_aioclient.get(
1039+
TEST_URL_GITHUB_v4,
1040+
status=200,
1041+
body=load_fixture("github_v4.json"),
1042+
)
1043+
with caplog.at_level(logging.DEBUG):
1044+
firmware = await test_charger_unknown_semver.firmware_check()
1045+
assert "Using version: random_a4f11e" in caplog.text
1046+
assert "Non-semver firmware version detected." in caplog.text
1047+
assert firmware is None
1048+
10011049

10021050
async def test_evse_restart(test_charger_v2, mock_aioclient, caplog):
10031051
"""Test EVSE module restart."""
@@ -1102,7 +1150,9 @@ async def test_max_current_soft(fixture, expected, request):
11021150
assert status == expected
11031151

11041152

1105-
async def test_set_override(test_charger, test_charger_v2, mock_aioclient, caplog):
1153+
async def test_set_override(
1154+
test_charger, test_charger_v2, test_charger_unknown_semver, mock_aioclient, caplog
1155+
):
11061156
"""Test set override function."""
11071157
await test_charger.update()
11081158
mock_aioclient.post(
@@ -1176,6 +1226,10 @@ async def test_set_override(test_charger, test_charger_v2, mock_aioclient, caplo
11761226
status = await test_charger_v2.set_override("active")
11771227
assert "Feature not supported for older firmware." in caplog.text
11781228

1229+
await test_charger_unknown_semver.update()
1230+
status = await test_charger_unknown_semver.set_override("active")
1231+
assert "Feature not supported for older firmware." in caplog.text
1232+
11791233

11801234
async def test_clear_override(test_charger, test_charger_v2, mock_aioclient, caplog):
11811235
"""Test clear override function."""

0 commit comments

Comments
 (0)