1 """Support for the Transmission BitTorrent client API."""
3 from __future__
import annotations
5 from functools
import partial
8 from typing
import Any, Final
10 import transmission_rpc
11 from transmission_rpc.error
import (
12 TransmissionAuthError,
13 TransmissionConnectError,
16 import voluptuous
as vol
32 ConfigEntryAuthFailed,
37 config_validation
as cv,
38 entity_registry
as er,
52 SERVICE_REMOVE_TORRENT,
53 SERVICE_START_TORRENT,
56 from .coordinator
import TransmissionDataUpdateCoordinator
57 from .errors
import AuthenticationError, CannotConnect, UnknownError
59 _LOGGER = logging.getLogger(__name__)
61 PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
63 MIGRATION_NAME_TO_KEY = {
65 "Down Speed":
"download",
68 "Active Torrents":
"active_torrents",
69 "Paused Torrents":
"paused_torrents",
70 "Total Torrents":
"total_torrents",
71 "Completed Torrents":
"completed_torrents",
72 "Started Torrents":
"started_torrents",
75 "Turtle Mode":
"turtle_mode",
78 SERVICE_BASE_SCHEMA = vol.Schema(
80 vol.Required(CONF_ENTRY_ID): selector.ConfigEntrySelector(),
84 SERVICE_ADD_TORRENT_SCHEMA = vol.All(
85 SERVICE_BASE_SCHEMA.extend({vol.Required(ATTR_TORRENT): cv.string}),
89 SERVICE_REMOVE_TORRENT_SCHEMA = vol.All(
90 SERVICE_BASE_SCHEMA.extend(
92 vol.Required(CONF_ID): cv.positive_int,
93 vol.Optional(ATTR_DELETE_DATA, default=DEFAULT_DELETE_DATA): cv.boolean,
98 SERVICE_START_TORRENT_SCHEMA = vol.All(
99 SERVICE_BASE_SCHEMA.extend({vol.Required(CONF_ID): cv.positive_int}),
102 SERVICE_STOP_TORRENT_SCHEMA = vol.All(
103 SERVICE_BASE_SCHEMA.extend(
105 vol.Required(CONF_ID): cv.positive_int,
110 CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
112 type TransmissionConfigEntry = ConfigEntry[TransmissionDataUpdateCoordinator]
115 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
116 """Set up the Transmission component."""
122 hass: HomeAssistant, config_entry: TransmissionConfigEntry
124 """Set up the Transmission Component."""
128 entity_entry: er.RegistryEntry,
129 ) -> dict[str, Any] |
None:
130 """Update unique ID of entity entry."""
131 if CONF_NAME
not in config_entry.data:
134 f
"{config_entry.data[CONF_HOST]}-{config_entry.data[CONF_NAME]} (?P<name>.+)",
135 entity_entry.unique_id,
138 if match
and (key := MIGRATION_NAME_TO_KEY.get(match.group(
"name"))):
139 return {
"new_unique_id": f
"{config_entry.entry_id}-{key}"}
142 await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
146 except CannotConnect
as error:
147 raise ConfigEntryNotReady
from error
148 except (AuthenticationError, UnknownError)
as error:
149 raise ConfigEntryAuthFailed
from error
152 await hass.async_add_executor_job(coordinator.init_torrent_list)
154 await coordinator.async_config_entry_first_refresh()
155 config_entry.runtime_data = coordinator
157 await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
163 """Unload Transmission Entry from config_entry."""
164 return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
168 """Migrate an old config entry."""
170 "Migrating from version %s.%s",
171 config_entry.version,
172 config_entry.minor_version,
175 if config_entry.version == 1:
177 if config_entry.minor_version < 2:
178 new = {**config_entry.data}
180 new[CONF_PATH] = DEFAULT_PATH
181 new[CONF_SSL] = DEFAULT_SSL
183 hass.config_entries.async_update_entry(
184 config_entry, data=new, version=1, minor_version=2
188 "Migration to version %s.%s successful",
189 config_entry.version,
190 config_entry.minor_version,
197 hass: HomeAssistant, entry_id: str
198 ) -> TransmissionDataUpdateCoordinator:
199 """Return coordinator for entry id."""
200 entry: TransmissionConfigEntry |
None = hass.config_entries.async_get_entry(
203 if entry
is None or entry.state
is not ConfigEntryState.LOADED:
205 return entry.runtime_data
209 """Home Assistant services."""
211 async
def add_torrent(service: ServiceCall) ->
None:
212 """Add new torrent to download."""
213 entry_id: str = service.data[CONF_ENTRY_ID]
215 torrent: str = service.data[ATTR_TORRENT]
216 if torrent.startswith(
217 (
"http",
"ftp:",
"magnet:")
218 )
or hass.config.is_allowed_path(torrent):
219 await hass.async_add_executor_job(coordinator.api.add_torrent, torrent)
220 await coordinator.async_request_refresh()
222 _LOGGER.warning(
"Could not add torrent: unsupported type or no permission")
224 async
def start_torrent(service: ServiceCall) ->
None:
226 entry_id: str = service.data[CONF_ENTRY_ID]
228 torrent_id = service.data[CONF_ID]
229 await hass.async_add_executor_job(coordinator.api.start_torrent, torrent_id)
230 await coordinator.async_request_refresh()
232 async
def stop_torrent(service: ServiceCall) ->
None:
234 entry_id: str = service.data[CONF_ENTRY_ID]
236 torrent_id = service.data[CONF_ID]
237 await hass.async_add_executor_job(coordinator.api.stop_torrent, torrent_id)
238 await coordinator.async_request_refresh()
240 async
def remove_torrent(service: ServiceCall) ->
None:
241 """Remove torrent."""
242 entry_id: str = service.data[CONF_ENTRY_ID]
244 torrent_id = service.data[CONF_ID]
245 delete_data = service.data[ATTR_DELETE_DATA]
246 await hass.async_add_executor_job(
247 partial(coordinator.api.remove_torrent, torrent_id, delete_data=delete_data)
249 await coordinator.async_request_refresh()
251 hass.services.async_register(
252 DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA
255 hass.services.async_register(
257 SERVICE_REMOVE_TORRENT,
259 schema=SERVICE_REMOVE_TORRENT_SCHEMA,
262 hass.services.async_register(
264 SERVICE_START_TORRENT,
266 schema=SERVICE_START_TORRENT_SCHEMA,
269 hass.services.async_register(
271 SERVICE_STOP_TORRENT,
273 schema=SERVICE_STOP_TORRENT_SCHEMA,
278 hass: HomeAssistant, entry: dict[str, Any]
279 ) -> transmission_rpc.Client:
280 """Get Transmission client."""
281 protocol: Final =
"https" if entry[CONF_SSL]
else "http"
282 host = entry[CONF_HOST]
283 port = entry[CONF_PORT]
284 path = entry[CONF_PATH]
285 username = entry.get(CONF_USERNAME)
286 password = entry.get(CONF_PASSWORD)
289 api = await hass.async_add_executor_job(
291 transmission_rpc.Client,
300 except TransmissionAuthError
as error:
301 _LOGGER.error(
"Credentials for Transmission client are not valid")
302 raise AuthenticationError
from error
303 except TransmissionConnectError
as error:
304 _LOGGER.error(
"Connecting to the Transmission client %s failed", host)
305 raise CannotConnect
from error
306 except TransmissionError
as error:
308 raise UnknownError
from error
309 _LOGGER.debug(
"Successfully connected to %s", host)
dict[str, str]|None update_unique_id(er.RegistryEntry entity_entry, str unique_id)
bool async_setup_entry(HomeAssistant hass, TransmissionConfigEntry config_entry)
TransmissionDataUpdateCoordinator _get_coordinator_from_service_data(HomeAssistant hass, str entry_id)
bool async_migrate_entry(HomeAssistant hass, ConfigEntry config_entry)
bool async_setup(HomeAssistant hass, ConfigType config)
None setup_hass_services(HomeAssistant hass)
transmission_rpc.Client get_api(HomeAssistant hass, dict[str, Any] entry)
bool async_unload_entry(HomeAssistant hass, ConfigEntry config_entry)