diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index bbcec432240..1dc879da3b8 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -25,7 +25,11 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryError, + ConfigEntryNotReady, +) from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -34,8 +38,9 @@ from homeassistant.helpers import ( from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.typing import ConfigType -from .const import DEFAULT_PATH, DEFAULT_SSL, DOMAIN +from .const import DEFAULT_PATH, DEFAULT_SSL, DOMAIN, MIN_REQUIRED_TRANSMISSION_VERSION from .coordinator import TransmissionConfigEntry, TransmissionDataUpdateCoordinator +from .helpers import create_version from .services import async_setup_services _LOGGER = logging.getLogger(__name__) @@ -97,6 +102,17 @@ async def async_setup_entry( except (TransmissionConnectError, TransmissionError) as err: raise ConfigEntryNotReady from err + version = create_version(api.server_version) + if version.valid and version < MIN_REQUIRED_TRANSMISSION_VERSION: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="version_error", + translation_placeholders={ + "transmission_version": api.server_version, + "min_version": MIN_REQUIRED_TRANSMISSION_VERSION, + }, + ) + protocol: Final = "https" if config_entry.data[CONF_SSL] else "http" device_registry = dr.async_get(hass) device_registry.async_get_or_create( diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index 9294319aeb8..29caef33269 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -40,8 +40,10 @@ from .const import ( DEFAULT_PORT, DEFAULT_SSL, DOMAIN, + MIN_REQUIRED_TRANSMISSION_VERSION, SUPPORTED_ORDER_MODES, ) +from .helpers import create_version DATA_SCHEMA = vol.Schema( { @@ -80,13 +82,17 @@ class TransmissionFlowHandler(ConfigFlow, domain=DOMAIN): {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} ) try: - await get_api(self.hass, user_input) + api = await get_api(self.hass, user_input) except TransmissionAuthError: errors[CONF_USERNAME] = "invalid_auth" errors[CONF_PASSWORD] = "invalid_auth" except TransmissionConnectError, TransmissionError: errors["base"] = "cannot_connect" + else: + version = create_version(api.server_version) + if version.valid and version < MIN_REQUIRED_TRANSMISSION_VERSION: + errors["base"] = "transmission_version" if not errors: return self.async_create_entry( @@ -115,14 +121,20 @@ class TransmissionFlowHandler(ConfigFlow, domain=DOMAIN): if user_input is not None: user_input = {**reauth_entry.data, **user_input} try: - await get_api(self.hass, user_input) + api = await get_api(self.hass, user_input) except TransmissionAuthError: errors[CONF_PASSWORD] = "invalid_auth" except TransmissionConnectError, TransmissionError: errors["base"] = "cannot_connect" else: - return self.async_update_reload_and_abort(reauth_entry, data=user_input) + version = create_version(api.server_version) + if version.valid and version < MIN_REQUIRED_TRANSMISSION_VERSION: + errors["base"] = "transmission_version" + else: + return self.async_update_reload_and_abort( + reauth_entry, data=user_input + ) return self.async_show_form( description_placeholders={ diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py index cde621249c3..ff29632c2e2 100644 --- a/homeassistant/components/transmission/const.py +++ b/homeassistant/components/transmission/const.py @@ -4,10 +4,13 @@ from __future__ import annotations from collections.abc import Callable +from awesomeversion import AwesomeVersion from transmission_rpc import Torrent DOMAIN = "transmission" +MIN_REQUIRED_TRANSMISSION_VERSION = AwesomeVersion("4.0.0") + ORDER_NEWEST_FIRST = "newest_first" ORDER_OLDEST_FIRST = "oldest_first" ORDER_BEST_RATIO_FIRST = "best_ratio_first" diff --git a/homeassistant/components/transmission/helpers.py b/homeassistant/components/transmission/helpers.py index 4a3ddc28b27..0fa111a6d52 100644 --- a/homeassistant/components/transmission/helpers.py +++ b/homeassistant/components/transmission/helpers.py @@ -2,6 +2,7 @@ from typing import Any +from awesomeversion import AwesomeVersion from transmission_rpc.torrent import Torrent @@ -43,3 +44,8 @@ def format_torrents( value[torrent.name] = format_torrent(torrent) return value + + +def create_version(version: str) -> AwesomeVersion: + """Convert versions, transmission has x.x.x (build).""" + return AwesomeVersion(version.split(" ", 1)[0]) diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index 509191c5349..34d229b9e61 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -6,7 +6,8 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "transmission_version": "Minimum required version is 4.0.0. Please upgrade Transmission and then retry." }, "step": { "reauth_confirm": { @@ -122,6 +123,9 @@ "exceptions": { "could_not_add_torrent": { "message": "Could not add torrent: unsupported type or no permission." + }, + "version_error": { + "message": "You are running {transmission_version} of Transmission. Minimum required version is {min_version}. Please upgrade Transmission and then restart Home Assistant." } }, "options": { diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 1692de2ae84..ea2b961fc48 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -160,6 +160,47 @@ async def test_flow_errors( assert result["type"] is FlowResultType.CREATE_ENTRY +@pytest.mark.parametrize( + ("version"), + [ + ("v1.0.0-RC2"), + ("v0.1.0"), + ("v1.9.0"), + ("3.0.0"), + ("3.0.0 (123798)"), + ], +) +async def test_flow_version_error( + hass: HomeAssistant, + mock_transmission_client: AsyncMock, + mock_setup_entry: AsyncMock, + version: str, +) -> None: + """Test flow version error.""" + mock_transmission_client.return_value.server_version = version + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG_DATA + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "transmission_version"} + + mock_transmission_client.return_value.server_version = "4.0.5 (a6fe2a64aa)" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_CONFIG_DATA, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + async def test_reauth_success( hass: HomeAssistant, mock_transmission_client: AsyncMock, @@ -244,3 +285,49 @@ async def test_reauth_flow_errors( }, ) assert result["type"] is FlowResultType.ABORT + + +@pytest.mark.parametrize( + ("version"), + [ + ("v1.0.0-RC2"), + ("v0.1.0"), + ("v1.9.0"), + ("3.0.0"), + ("3.0.0 (123798)"), + ], +) +async def test_reauth_version_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_transmission_client: AsyncMock, + version: str, +) -> None: + """Test reauth version error.""" + mock_transmission_client.return_value.server_version = version + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reauth_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "password": "test-password", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "transmission_version"} + + mock_transmission_client.return_value.server_version = "4.0.5 (a6fe2a64aa)" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "password": "test-password", + }, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" diff --git a/tests/components/transmission/test_init.py b/tests/components/transmission/test_init.py index 653f77e2811..2da396339b0 100644 --- a/tests/components/transmission/test_init.py +++ b/tests/components/transmission/test_init.py @@ -80,6 +80,32 @@ async def test_setup_failed_auth_error( assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR +@pytest.mark.parametrize( + ("version"), + [ + ("v1.0.0-RC2"), + ("v0.1.0"), + ("v1.9.0"), + ("3.0.0"), + ("3.0.0 (123798)"), + ], +) +async def test_setup_failed_too_old( + hass: HomeAssistant, + mock_transmission_client: AsyncMock, + mock_config_entry: MockConfigEntry, + version: str, +) -> None: + """Test setup of Transmission entry with too old version of Transmission.""" + mock_config_entry.add_to_hass(hass) + + mock_transmission_client.return_value.server_version = version + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + async def test_setup_failed_unexpected_error( hass: HomeAssistant, mock_transmission_client: AsyncMock,