diff --git a/openevsehttp/__main__.py b/openevsehttp/__main__.py index 3f4be69..a05a141 100644 --- a/openevsehttp/__main__.py +++ b/openevsehttp/__main__.py @@ -10,6 +10,7 @@ import aiohttp # type: ignore from aiohttp.client_exceptions import ContentTypeError, ServerTimeoutError from awesomeversion import AwesomeVersion +from awesomeversion.exceptions import AwesomeVersionCompareException from .const import MAX_AMPS, MIN_AMPS from .exceptions import ( @@ -489,15 +490,22 @@ async def firmware_check(self) -> dict | None: _LOGGER.debug("Stripping 'dev' from version.") value = value.split(".") value = ".".join(value[0:3]) - _LOGGER.debug("Using version: %s", value) - current = AwesomeVersion(value) + elif "master" in self._config["version"]: + value = "dev" else: - current = AwesomeVersion(self._config["version"]) + value = self._config["version"] - if current >= cutoff: - url = f"{base_url}ESP32_WiFi_V4.x/releases/latest" - else: - url = f"{base_url}ESP8266_WiFi_v2.x/releases/latest" + _LOGGER.debug("Using version: %s", value) + current = AwesomeVersion(value) + + try: + if current >= cutoff: + url = f"{base_url}ESP32_WiFi_V4.x/releases/latest" + else: + url = f"{base_url}ESP8266_WiFi_v2.x/releases/latest" + except AwesomeVersionCompareException: + _LOGGER.warning("Non-semver firmware version detected.") + return None try: async with aiohttp.ClientSession() as session: @@ -551,16 +559,26 @@ def _version_check(self, min_version: str, max_version: str = "") -> bool: _LOGGER.debug("Stripping 'dev' from version.") value = value.split(".") value = ".".join(value[0:3]) - current = AwesomeVersion(value) + elif "master" in self._config["version"]: + value = "dev" else: - current = AwesomeVersion(self._config["version"]) + value = self._config["version"] + + current = AwesomeVersion(value) if limit: - if cutoff <= current <= limit: - return True + try: + if cutoff <= current <= limit: + return True + except AwesomeVersionCompareException: + _LOGGER.debug("Non-semver firmware version detected.") return False - if current >= cutoff: - return True + + try: + if current >= cutoff: + return True + except AwesomeVersionCompareException: + _LOGGER.debug("Non-semver firmware version detected.") return False @property diff --git a/setup.py b/setup.py index e771fa4..e6a3031 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ PROJECT_DIR = Path(__file__).parent.resolve() README_FILE = PROJECT_DIR / "README.md" -VERSION = "0.1.50" +VERSION = "0.1.51" setup( name="python-openevse-http", diff --git a/tests/conftest.py b/tests/conftest.py index a9da66e..73ac412 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -107,6 +107,38 @@ def test_charger_broken(mock_aioclient): return main.OpenEVSE(TEST_TLD) +@pytest.fixture(name="test_charger_broken_semver") +def test_charger_broken_semver(mock_aioclient): + """Load the charger data.""" + mock_aioclient.get( + TEST_URL_STATUS, + status=200, + body=load_fixture("v4_json/status.json"), + ) + mock_aioclient.get( + TEST_URL_CONFIG, + status=200, + body=load_fixture("v4_json/config-broken-semver.json"), + ) + return main.OpenEVSE(TEST_TLD) + + +@pytest.fixture(name="test_charger_unknown_semver") +def test_charger_unknown_semver(mock_aioclient): + """Load the charger data.""" + mock_aioclient.get( + TEST_URL_STATUS, + status=200, + body=load_fixture("v4_json/status.json"), + ) + mock_aioclient.get( + TEST_URL_CONFIG, + status=200, + body=load_fixture("v4_json/config-unknown-semver.json"), + ) + return main.OpenEVSE(TEST_TLD) + + @pytest.fixture(name="test_charger_v2") def test_charger_v2(mock_aioclient): """Load the charger data.""" diff --git a/tests/fixtures/v4_json/config-broken-semver.json b/tests/fixtures/v4_json/config-broken-semver.json new file mode 100644 index 0000000..a486cc2 --- /dev/null +++ b/tests/fixtures/v4_json/config-broken-semver.json @@ -0,0 +1,66 @@ +{ + "firmware": "7.1.3", + "protocol": "-", + "espflash": 4194304, + "wifi_serial": "1234567890AB", + "version": "master_abcd123", + "diodet": 0, + "gfcit": 0, + "groundt": 0, + "relayt": 0, + "ventt": 0, + "tempt": 0, + "service": 2, + "scale": 220, + "offset": 0, + "max_current_soft": 48, + "min_current_hard": 6, + "max_current_hard": 48, + "mqtt_supported_protocols": [ + "mqtt", + "mqtts" + ], + "http_supported_protocols": [ + "http", + "https" + ], + "ssid": "Datanode-IoT", + "pass": "_DUMMY_PASSWORD", + "www_username": "", + "www_password": "", + "hostname": "openevse-7b2c", + "sntp_hostname": "0.us.pool.ntp.org", + "time_zone": "America/Phoenix|MST7", + "emoncms_server": "https://emoncms.collective.lan/", + "emoncms_node": "openevse", + "emoncms_apikey": "_DUMMY_PASSWORD", + "emoncms_fingerprint": "", + "mqtt_server": "192.168.1.198", + "mqtt_port": 1883, + "mqtt_topic": "openevse", + "mqtt_user": "devices", + "mqtt_pass": "_DUMMY_PASSWORD", + "mqtt_solar": "", + "mqtt_grid_ie": "home-assistant/power/watts", + "mqtt_vrms": "home-assistant/solar/watts", + "mqtt_announce_topic": "openevse/announce/7b2c", + "ohm": "", + "divert_PV_ratio": 1.1, + "divert_attack_smoothing_factor": 0.4, + "divert_decay_smoothing_factor": 0.05, + "divert_min_charge_time": 600, + "tesla_username": "", + "tesla_password": "", + "tesla_vehidx": -1, + "led_brightness": 128, + "flags": 522, + "emoncms_enabled": false, + "mqtt_enabled": true, + "mqtt_reject_unauthorized": true, + "ohm_enabled": false, + "sntp_enabled": true, + "tesla_enabled": false, + "pause_uses_disabled": false, + "mqtt_protocol": "mqtt", + "charge_mode": "fast" + } diff --git a/tests/fixtures/v4_json/config-unknown-semver.json b/tests/fixtures/v4_json/config-unknown-semver.json new file mode 100644 index 0000000..60d1aff --- /dev/null +++ b/tests/fixtures/v4_json/config-unknown-semver.json @@ -0,0 +1,66 @@ +{ + "firmware": "7.1.3", + "protocol": "-", + "espflash": 4194304, + "wifi_serial": "1234567890AB", + "version": "random_a4f11e", + "diodet": 0, + "gfcit": 0, + "groundt": 0, + "relayt": 0, + "ventt": 0, + "tempt": 0, + "service": 2, + "scale": 220, + "offset": 0, + "max_current_soft": 48, + "min_current_hard": 6, + "max_current_hard": 48, + "mqtt_supported_protocols": [ + "mqtt", + "mqtts" + ], + "http_supported_protocols": [ + "http", + "https" + ], + "ssid": "Datanode-IoT", + "pass": "_DUMMY_PASSWORD", + "www_username": "", + "www_password": "", + "hostname": "openevse-7b2c", + "sntp_hostname": "0.us.pool.ntp.org", + "time_zone": "America/Phoenix|MST7", + "emoncms_server": "https://emoncms.collective.lan/", + "emoncms_node": "openevse", + "emoncms_apikey": "_DUMMY_PASSWORD", + "emoncms_fingerprint": "", + "mqtt_server": "192.168.1.198", + "mqtt_port": 1883, + "mqtt_topic": "openevse", + "mqtt_user": "devices", + "mqtt_pass": "_DUMMY_PASSWORD", + "mqtt_solar": "", + "mqtt_grid_ie": "home-assistant/power/watts", + "mqtt_vrms": "home-assistant/solar/watts", + "mqtt_announce_topic": "openevse/announce/7b2c", + "ohm": "", + "divert_PV_ratio": 1.1, + "divert_attack_smoothing_factor": 0.4, + "divert_decay_smoothing_factor": 0.05, + "divert_min_charge_time": 600, + "tesla_username": "", + "tesla_password": "", + "tesla_vehidx": -1, + "led_brightness": 128, + "flags": 522, + "emoncms_enabled": false, + "mqtt_enabled": true, + "mqtt_reject_unauthorized": true, + "ohm_enabled": false, + "sntp_enabled": true, + "tesla_enabled": false, + "pause_uses_disabled": false, + "mqtt_protocol": "mqtt", + "charge_mode": "fast" + } diff --git a/tests/test_main.py b/tests/test_main.py index 6b57588..1f22f54 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -7,6 +7,8 @@ import logging from unittest import mock +from awesomeversion.exceptions import AwesomeVersionCompareException + import pytest from aiohttp.client_exceptions import ContentTypeError, ServerTimeoutError @@ -230,6 +232,7 @@ async def test_get_service_level(fixture, expected, request): ("test_charger", "4.1.2"), ("test_charger_v2", "2.9.1"), ("test_charger_dev", "4.1.5"), + ("test_charger_broken_semver", "master_abcd123"), ], ) async def test_get_wifi_firmware(fixture, expected, request): @@ -641,7 +644,12 @@ async def test_get_manual_override(fixture, expected, request): async def test_toggle_override( - test_charger, test_charger_dev, test_charger_new, mock_aioclient, caplog + test_charger, + test_charger_dev, + test_charger_new, + test_charger_unknown_semver, + mock_aioclient, + caplog, ): """Test v4 Status reply.""" await test_charger.update() @@ -848,7 +856,12 @@ async def test_get_charging_power(fixture, expected, request): async def test_set_divertmode( - test_charger_new, test_charger_v2, test_charger_broken, mock_aioclient, caplog + test_charger_new, + test_charger_v2, + test_charger_broken, + test_charger_unknown_semver, + mock_aioclient, + caplog, ): """Test v4 set divert mode.""" await test_charger_new.update() @@ -896,6 +909,17 @@ async def test_set_divertmode( with pytest.raises(UnsupportedFeature): await test_charger_broken.divert_mode() + mock_aioclient.post( + TEST_URL_CONFIG, + status=200, + body=value, + ) + await test_charger_unknown_semver.update() + with pytest.raises(UnsupportedFeature): + with caplog.at_level(logging.DEBUG): + await test_charger_unknown_semver.divert_mode() + assert "Non-semver firmware version detected." in caplog.text + async def test_test_and_get(test_charger, test_charger_v2, mock_aioclient, caplog): """Test v4 Status reply""" @@ -931,6 +955,8 @@ async def test_firmware_check( test_charger_dev, test_charger_v2, test_charger_broken, + test_charger_broken_semver, + test_charger_unknown_semver, mock_aioclient, caplog, ): @@ -998,6 +1024,28 @@ async def test_firmware_check( assert "Unable to find firmware version." in caplog.text assert firmware is None + await test_charger_broken_semver.update() + mock_aioclient.get( + TEST_URL_GITHUB_v4, + status=200, + body=load_fixture("github_v4.json"), + ) + firmware = await test_charger_broken_semver.firmware_check() + assert firmware["latest_version"] == "4.1.4" + + await test_charger_unknown_semver.update() + assert test_charger_unknown_semver.wifi_firmware == "random_a4f11e" + 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 "Non-semver firmware version detected." in caplog.text + assert firmware is None + async def test_evse_restart(test_charger_v2, mock_aioclient, caplog): """Test EVSE module restart.""" @@ -1102,7 +1150,9 @@ async def test_max_current_soft(fixture, expected, request): assert status == expected -async def test_set_override(test_charger, test_charger_v2, mock_aioclient, caplog): +async def test_set_override( + test_charger, test_charger_v2, test_charger_unknown_semver, mock_aioclient, caplog +): """Test set override function.""" await test_charger.update() mock_aioclient.post( @@ -1176,6 +1226,10 @@ async def test_set_override(test_charger, test_charger_v2, mock_aioclient, caplo status = await test_charger_v2.set_override("active") assert "Feature not supported for older firmware." in caplog.text + await test_charger_unknown_semver.update() + status = await test_charger_unknown_semver.set_override("active") + assert "Feature not supported for older firmware." in caplog.text + async def test_clear_override(test_charger, test_charger_v2, mock_aioclient, caplog): """Test clear override function."""