diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index 0678da44472..61ebc144e3d 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -9,6 +9,7 @@ from typing import Any, Final, cast from aioshelly.const import RPC_GENERATIONS from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError +from awesomeversion import AwesomeVersion, AwesomeVersionStrategy from homeassistant.components.update import ( ATTR_INSTALLED_VERSION, @@ -203,6 +204,22 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity): else: LOGGER.debug("Result of OTA update call: %s", result) + def version_is_newer(self, latest_version: str, installed_version: str) -> bool: + """Return True if available version is newer then installed version. + + Default strategy generate an exception with Shelly firmware format + thus making the entity state always true. + """ + return AwesomeVersion( + latest_version, + find_first_match=True, + ensure_strategy=[AwesomeVersionStrategy.SEMVER], + ) > AwesomeVersion( + installed_version, + find_first_match=True, + ensure_strategy=[AwesomeVersionStrategy.SEMVER], + ) + class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): """Represent a RPC update entity.""" diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index a983cbbcda9..d453d25698c 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -226,9 +226,9 @@ MOCK_STATUS_COAP = { "update": { "status": "pending", "has_update": True, - "beta_version": "some_beta_version", - "new_version": "some_new_version", - "old_version": "some_old_version", + "beta_version": "20231107-162609/v1.14.1-rc1-g0617c15", + "new_version": "20230913-111730/v1.14.0-gcb84623", + "old_version": "20230913-111730/v1.14.0-gcb84623", }, "uptime": 5 * REST_SENSORS_UPDATE_INTERVAL, "wifi_sta": {"rssi": -64}, diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index c6434c0b988..b4145b2441a 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -54,15 +54,15 @@ async def test_block_update( ) -> None: """Test block device update entity.""" entity_id = "update.test_name_firmware_update" - monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1") - monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2") + monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1.0.0") + monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2.0.0") monkeypatch.setitem(mock_block_device.status, "cloud", {"connected": False}) await init_integration(hass, 1) state = hass.states.get(entity_id) assert state.state == STATE_ON - assert state.attributes[ATTR_INSTALLED_VERSION] == "1" - assert state.attributes[ATTR_LATEST_VERSION] == "2" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "2.0.0" assert state.attributes[ATTR_IN_PROGRESS] is False supported_feat = state.attributes[ATTR_SUPPORTED_FEATURES] assert supported_feat == UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS @@ -77,18 +77,18 @@ async def test_block_update( state = hass.states.get(entity_id) assert state.state == STATE_ON - assert state.attributes[ATTR_INSTALLED_VERSION] == "1" - assert state.attributes[ATTR_LATEST_VERSION] == "2" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "2.0.0" assert state.attributes[ATTR_IN_PROGRESS] is True assert state.attributes[ATTR_RELEASE_URL] == GEN1_RELEASE_URL - monkeypatch.setitem(mock_block_device.status["update"], "old_version", "2") + monkeypatch.setitem(mock_block_device.status["update"], "old_version", "2.0.0") await mock_rest_update(hass, freezer) state = hass.states.get(entity_id) assert state.state == STATE_OFF - assert state.attributes[ATTR_INSTALLED_VERSION] == "2" - assert state.attributes[ATTR_LATEST_VERSION] == "2" + assert state.attributes[ATTR_INSTALLED_VERSION] == "2.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "2.0.0" assert state.attributes[ATTR_IN_PROGRESS] is False entry = entity_registry.async_get(entity_id) @@ -106,25 +106,27 @@ async def test_block_beta_update( ) -> None: """Test block device beta update entity.""" entity_id = "update.test_name_beta_firmware_update" - monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1") - monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2") + monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1.0.0") + monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2.0.0") monkeypatch.setitem(mock_block_device.status["update"], "beta_version", "") monkeypatch.setitem(mock_block_device.status, "cloud", {"connected": False}) await init_integration(hass, 1) state = hass.states.get(entity_id) assert state.state == STATE_OFF - assert state.attributes[ATTR_INSTALLED_VERSION] == "1" - assert state.attributes[ATTR_LATEST_VERSION] == "1" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.0" assert state.attributes[ATTR_IN_PROGRESS] is False - monkeypatch.setitem(mock_block_device.status["update"], "beta_version", "2b") + monkeypatch.setitem( + mock_block_device.status["update"], "beta_version", "2.0.0-beta" + ) await mock_rest_update(hass, freezer) state = hass.states.get(entity_id) assert state.state == STATE_ON - assert state.attributes[ATTR_INSTALLED_VERSION] == "1" - assert state.attributes[ATTR_LATEST_VERSION] == "2b" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "2.0.0-beta" assert state.attributes[ATTR_IN_PROGRESS] is False assert state.attributes[ATTR_RELEASE_URL] is None @@ -138,17 +140,17 @@ async def test_block_beta_update( state = hass.states.get(entity_id) assert state.state == STATE_ON - assert state.attributes[ATTR_INSTALLED_VERSION] == "1" - assert state.attributes[ATTR_LATEST_VERSION] == "2b" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "2.0.0-beta" assert state.attributes[ATTR_IN_PROGRESS] is True - monkeypatch.setitem(mock_block_device.status["update"], "old_version", "2b") + monkeypatch.setitem(mock_block_device.status["update"], "old_version", "2.0.0-beta") await mock_rest_update(hass, freezer) state = hass.states.get(entity_id) assert state.state == STATE_OFF - assert state.attributes[ATTR_INSTALLED_VERSION] == "2b" - assert state.attributes[ATTR_LATEST_VERSION] == "2b" + assert state.attributes[ATTR_INSTALLED_VERSION] == "2.0.0-beta" + assert state.attributes[ATTR_LATEST_VERSION] == "2.0.0-beta" assert state.attributes[ATTR_IN_PROGRESS] is False entry = entity_registry.async_get(entity_id) @@ -164,8 +166,8 @@ async def test_block_update_connection_error( caplog: pytest.LogCaptureFixture, ) -> None: """Test block device update connection error.""" - monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1") - monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2") + monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1.0.0") + monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2.0.0") monkeypatch.setattr( mock_block_device, "trigger_ota_update", @@ -190,8 +192,8 @@ async def test_block_update_auth_error( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block device update authentication error.""" - monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1") - monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2") + monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1.0.0") + monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2.0.0") monkeypatch.setattr( mock_block_device, "trigger_ota_update", @@ -222,6 +224,51 @@ async def test_block_update_auth_error( assert flow["context"].get("entry_id") == entry.entry_id +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_block_version_compare( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_block_device: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test block device custom firmware version comparison.""" + + STABLE = "20230913-111730/v1.14.0-gcb84623" + BETA = "20231107-162609/v1.14.1-rc1-g0617c15" + + entity_id_beta = "update.test_name_beta_firmware_update" + entity_id_latest = "update.test_name_firmware_update" + monkeypatch.setitem(mock_block_device.status["update"], "old_version", STABLE) + monkeypatch.setitem(mock_block_device.status["update"], "new_version", "") + monkeypatch.setitem(mock_block_device.status["update"], "beta_version", BETA) + monkeypatch.setitem(mock_block_device.status, "cloud", {"connected": False}) + await init_integration(hass, 1) + + state = hass.states.get(entity_id_latest) + assert state.state == STATE_OFF + assert state.attributes[ATTR_INSTALLED_VERSION] == STABLE + assert state.attributes[ATTR_LATEST_VERSION] == STABLE + state = hass.states.get(entity_id_beta) + assert state.state == STATE_ON + assert state.attributes[ATTR_INSTALLED_VERSION] == STABLE + assert state.attributes[ATTR_LATEST_VERSION] == BETA + + monkeypatch.setitem(mock_block_device.status["update"], "old_version", BETA) + monkeypatch.setitem(mock_block_device.status["update"], "new_version", STABLE) + monkeypatch.setitem(mock_block_device.status["update"], "beta_version", BETA) + await mock_rest_update(hass, freezer) + + state = hass.states.get(entity_id_latest) + assert state.state == STATE_OFF + assert state.attributes[ATTR_INSTALLED_VERSION] == BETA + assert state.attributes[ATTR_LATEST_VERSION] == STABLE + state = hass.states.get(entity_id_beta) + assert state.state == STATE_OFF + assert state.attributes[ATTR_INSTALLED_VERSION] == BETA + assert state.attributes[ATTR_LATEST_VERSION] == BETA + + async def test_rpc_update( hass: HomeAssistant, mock_rpc_device: Mock,