diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 48d4a0e5163..8f81382012d 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -3,12 +3,13 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any import transmissionrpc from transmissionrpc.error import TransmissionError import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( CONF_HOST, CONF_ID, @@ -21,13 +22,15 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, selector from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from .const import ( ATTR_DELETE_DATA, ATTR_TORRENT, + CONF_ENTRY_ID, CONF_LIMIT, CONF_ORDER, DATA_UPDATED, @@ -49,30 +52,41 @@ from .errors import AuthenticationError, CannotConnect, UnknownError _LOGGER = logging.getLogger(__name__) -SERVICE_ADD_TORRENT_SCHEMA = vol.Schema( - {vol.Required(ATTR_TORRENT): cv.string, vol.Required(CONF_NAME): cv.string} -) - -SERVICE_REMOVE_TORRENT_SCHEMA = vol.Schema( +SERVICE_BASE_SCHEMA = vol.Schema( { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ID): cv.positive_int, - vol.Optional(ATTR_DELETE_DATA, default=DEFAULT_DELETE_DATA): cv.boolean, + vol.Exclusive(CONF_ENTRY_ID, "identifier"): selector.ConfigEntrySelector(), + vol.Exclusive(CONF_NAME, "identifier"): selector.TextSelector(), } ) -SERVICE_START_TORRENT_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ID): cv.positive_int, - } +SERVICE_ADD_TORRENT_SCHEMA = vol.All( + SERVICE_BASE_SCHEMA.extend({vol.Required(ATTR_TORRENT): cv.string}), + cv.has_at_least_one_key(CONF_ENTRY_ID, CONF_NAME), ) -SERVICE_STOP_TORRENT_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ID): cv.positive_int, - } + +SERVICE_REMOVE_TORRENT_SCHEMA = vol.All( + SERVICE_BASE_SCHEMA.extend( + { + vol.Required(CONF_ID): cv.positive_int, + vol.Optional(ATTR_DELETE_DATA, default=DEFAULT_DELETE_DATA): cv.boolean, + } + ), + cv.has_at_least_one_key(CONF_ENTRY_ID, CONF_NAME), +) + +SERVICE_START_TORRENT_SCHEMA = vol.All( + SERVICE_BASE_SCHEMA.extend({vol.Required(CONF_ID): cv.positive_int}), + cv.has_at_least_one_key(CONF_ENTRY_ID, CONF_NAME), +) + +SERVICE_STOP_TORRENT_SCHEMA = vol.All( + SERVICE_BASE_SCHEMA.extend( + { + vol.Required(CONF_ID): cv.positive_int, + } + ), + cv.has_at_least_one_key(CONF_ENTRY_ID, CONF_NAME), ) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @@ -135,6 +149,39 @@ async def get_api(hass, entry): raise UnknownError from error +def _get_client(hass: HomeAssistant, data: dict[str, Any]) -> TransmissionClient | None: + """Return client from integration name or entry_id.""" + if ( + (entry_id := data.get(CONF_ENTRY_ID)) + and (entry := hass.config_entries.async_get_entry(entry_id)) + and entry.state == ConfigEntryState.LOADED + ): + return hass.data[DOMAIN][entry_id] + + # to be removed once name key is removed + if CONF_NAME in data: + create_issue( + hass, + DOMAIN, + "deprecated_key", + breaks_in_ha_version="2023.1.0", + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key="deprecated_key", + ) + + _LOGGER.warning( + 'The "name" key in the Transmission services is deprecated and will be removed in "2023.1.0"; ' + 'use the "entry_id" key instead to identity which entry to call' + ) + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_NAME] == data[CONF_NAME]: + return hass.data[DOMAIN][entry.entry_id] + + return None + + class TransmissionClient: """Transmission Client Object.""" @@ -174,14 +221,9 @@ class TransmissionClient: def add_torrent(service: ServiceCall) -> None: """Add new torrent to download.""" - tm_client = None - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_NAME] == service.data[CONF_NAME]: - tm_client = self.hass.data[DOMAIN][entry.entry_id] - break - if tm_client is None: - _LOGGER.error("Transmission instance is not found") - return + if not (tm_client := _get_client(self.hass, service.data)): + raise ValueError("Transmission instance is not found") + torrent = service.data[ATTR_TORRENT] if torrent.startswith( ("http", "ftp:", "magnet:") @@ -195,42 +237,27 @@ class TransmissionClient: def start_torrent(service: ServiceCall) -> None: """Start torrent.""" - tm_client = None - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_NAME] == service.data[CONF_NAME]: - tm_client = self.hass.data[DOMAIN][entry.entry_id] - break - if tm_client is None: - _LOGGER.error("Transmission instance is not found") - return + if not (tm_client := _get_client(self.hass, service.data)): + raise ValueError("Transmission instance is not found") + torrent_id = service.data[CONF_ID] tm_client.tm_api.start_torrent(torrent_id) tm_client.api.update() def stop_torrent(service: ServiceCall) -> None: """Stop torrent.""" - tm_client = None - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_NAME] == service.data[CONF_NAME]: - tm_client = self.hass.data[DOMAIN][entry.entry_id] - break - if tm_client is None: - _LOGGER.error("Transmission instance is not found") - return + if not (tm_client := _get_client(self.hass, service.data)): + raise ValueError("Transmission instance is not found") + torrent_id = service.data[CONF_ID] tm_client.tm_api.stop_torrent(torrent_id) tm_client.api.update() def remove_torrent(service: ServiceCall) -> None: """Remove torrent.""" - tm_client = None - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_NAME] == service.data[CONF_NAME]: - tm_client = self.hass.data[DOMAIN][entry.entry_id] - break - if tm_client is None: - _LOGGER.error("Transmission instance is not found") - return + if not (tm_client := _get_client(self.hass, service.data)): + raise ValueError("Transmission instance is not found") + torrent_id = service.data[CONF_ID] delete_data = service.data[ATTR_DELETE_DATA] tm_client.tm_api.remove_torrent(torrent_id, delete_data=delete_data) diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py index 185148f3bd9..742ef874a35 100644 --- a/homeassistant/components/transmission/const.py +++ b/homeassistant/components/transmission/const.py @@ -18,7 +18,7 @@ SUPPORTED_ORDER_MODES = { torrents, key=lambda t: t.ratio, reverse=True ), } - +CONF_ENTRY_ID = "entry_id" CONF_LIMIT = "limit" CONF_ORDER = "order" diff --git a/homeassistant/components/transmission/services.yaml b/homeassistant/components/transmission/services.yaml index 74861df5a70..2fd4793c785 100644 --- a/homeassistant/components/transmission/services.yaml +++ b/homeassistant/components/transmission/services.yaml @@ -2,10 +2,15 @@ add_torrent: name: Add torrent description: Add a new torrent to download (URL, magnet link or Base64 encoded). fields: + entry_id: + name: Transmission entry + description: Config entry id + selector: + config_entry: + integration: transmission name: name: Name description: Instance name as entered during entry config - required: true example: Transmission selector: text: @@ -21,10 +26,15 @@ remove_torrent: name: Remove torrent description: Remove a torrent fields: + entry_id: + name: Transmission entry + description: Config entry id + selector: + config_entry: + integration: transmission name: name: Name description: Instance name as entered during entry config - required: true example: Transmission selector: text: @@ -46,6 +56,12 @@ start_torrent: name: Start torrent description: Start a torrent fields: + entry_id: + name: Transmission entry + description: Config entry id + selector: + config_entry: + integration: transmission name: name: Name description: Instance name as entered during entry config @@ -63,10 +79,15 @@ stop_torrent: name: Stop torrent description: Stop a torrent fields: + entry_id: + name: Transmission entry + description: Config entry id + selector: + config_entry: + integration: transmission name: name: Name description: Instance name as entered during entry config - required: true example: Transmission selector: text: diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index 0194917c416..a8ba9e5fcb3 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -40,5 +40,18 @@ } } } + }, + "issues": { + "deprecated_key": { + "title": "The name key in Transmission services is being removed", + "fix_flow": { + "step": { + "confirm": { + "title": "The name key in Transmission services is being removed", + "description": "Update any automations or scripts that use this service and replace the name key with the entry_id key." + } + } + } + } } } diff --git a/homeassistant/components/transmission/translations/en.json b/homeassistant/components/transmission/translations/en.json index 3726f6f0a7e..46ab2c64ad9 100644 --- a/homeassistant/components/transmission/translations/en.json +++ b/homeassistant/components/transmission/translations/en.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Update any automations or scripts that use this service and replace the name key with the entry_id key.", + "title": "The name key in Transmission services is being removed" + } + } + }, + "title": "The name key in Transmission services is being removed" + } + }, "options": { "step": { "init": {