core/homeassistant/components/transmission/__init__.py

244 lines
7.3 KiB
Python

"""Support for the Transmission BitTorrent client API."""
from __future__ import annotations
from functools import partial
import logging
import re
from typing import Any
import transmission_rpc
from transmission_rpc.error import (
TransmissionAuthError,
TransmissionConnectError,
TransmissionError,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_ID,
CONF_NAME,
CONF_PASSWORD,
CONF_PORT,
CONF_USERNAME,
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import (
config_validation as cv,
entity_registry as er,
selector,
)
from .const import (
ATTR_DELETE_DATA,
ATTR_TORRENT,
CONF_ENTRY_ID,
DEFAULT_DELETE_DATA,
DOMAIN,
SERVICE_ADD_TORRENT,
SERVICE_REMOVE_TORRENT,
SERVICE_START_TORRENT,
SERVICE_STOP_TORRENT,
)
from .coordinator import TransmissionDataUpdateCoordinator
from .errors import AuthenticationError, CannotConnect, UnknownError
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
MIGRATION_NAME_TO_KEY = {
# Sensors
"Down Speed": "download",
"Up Speed": "upload",
"Status": "status",
"Active Torrents": "active_torrents",
"Paused Torrents": "paused_torrents",
"Total Torrents": "total_torrents",
"Completed Torrents": "completed_torrents",
"Started Torrents": "started_torrents",
# Switches
"Switch": "on_off",
"Turtle Mode": "turtle_mode",
}
SERVICE_BASE_SCHEMA = vol.Schema(
{
vol.Required(CONF_ENTRY_ID): selector.ConfigEntrySelector(),
}
)
SERVICE_ADD_TORRENT_SCHEMA = vol.All(
SERVICE_BASE_SCHEMA.extend({vol.Required(ATTR_TORRENT): cv.string}),
)
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,
}
)
)
SERVICE_START_TORRENT_SCHEMA = vol.All(
SERVICE_BASE_SCHEMA.extend({vol.Required(CONF_ID): cv.positive_int}),
)
SERVICE_STOP_TORRENT_SCHEMA = vol.All(
SERVICE_BASE_SCHEMA.extend(
{
vol.Required(CONF_ID): cv.positive_int,
}
)
)
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up the Transmission Component."""
@callback
def update_unique_id(
entity_entry: er.RegistryEntry,
) -> dict[str, Any] | None:
"""Update unique ID of entity entry."""
if CONF_NAME not in config_entry.data:
return None
match = re.search(
f"{config_entry.data[CONF_HOST]}-{config_entry.data[CONF_NAME]} (?P<name>.+)",
entity_entry.unique_id,
)
if match and (key := MIGRATION_NAME_TO_KEY.get(match.group("name"))):
return {"new_unique_id": f"{config_entry.entry_id}-{key}"}
return None
await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
try:
api = await get_api(hass, dict(config_entry.data))
except CannotConnect as error:
raise ConfigEntryNotReady from error
except (AuthenticationError, UnknownError) as error:
raise ConfigEntryAuthFailed from error
coordinator = TransmissionDataUpdateCoordinator(hass, config_entry, api)
await hass.async_add_executor_job(coordinator.init_torrent_list)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
async def add_torrent(service: ServiceCall) -> None:
"""Add new torrent to download."""
torrent = service.data[ATTR_TORRENT]
if torrent.startswith(
("http", "ftp:", "magnet:")
) or hass.config.is_allowed_path(torrent):
await hass.async_add_executor_job(coordinator.api.add_torrent, torrent)
await coordinator.async_request_refresh()
else:
_LOGGER.warning("Could not add torrent: unsupported type or no permission")
async def start_torrent(service: ServiceCall) -> None:
"""Start torrent."""
torrent_id = service.data[CONF_ID]
await hass.async_add_executor_job(coordinator.api.start_torrent, torrent_id)
await coordinator.async_request_refresh()
async def stop_torrent(service: ServiceCall) -> None:
"""Stop torrent."""
torrent_id = service.data[CONF_ID]
await hass.async_add_executor_job(coordinator.api.stop_torrent, torrent_id)
await coordinator.async_request_refresh()
async def remove_torrent(service: ServiceCall) -> None:
"""Remove torrent."""
torrent_id = service.data[CONF_ID]
delete_data = service.data[ATTR_DELETE_DATA]
await hass.async_add_executor_job(
partial(coordinator.api.remove_torrent, torrent_id, delete_data=delete_data)
)
await coordinator.async_request_refresh()
hass.services.async_register(
DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA
)
hass.services.async_register(
DOMAIN,
SERVICE_REMOVE_TORRENT,
remove_torrent,
schema=SERVICE_REMOVE_TORRENT_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_START_TORRENT,
start_torrent,
schema=SERVICE_START_TORRENT_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_STOP_TORRENT,
stop_torrent,
schema=SERVICE_STOP_TORRENT_SCHEMA,
)
return True
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload Transmission Entry from config_entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
):
hass.data[DOMAIN].pop(config_entry.entry_id)
if not hass.data[DOMAIN]:
hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT)
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_TORRENT)
hass.services.async_remove(DOMAIN, SERVICE_START_TORRENT)
hass.services.async_remove(DOMAIN, SERVICE_STOP_TORRENT)
return unload_ok
async def get_api(
hass: HomeAssistant, entry: dict[str, Any]
) -> transmission_rpc.Client:
"""Get Transmission client."""
host = entry[CONF_HOST]
port = entry[CONF_PORT]
username = entry.get(CONF_USERNAME)
password = entry.get(CONF_PASSWORD)
try:
api = await hass.async_add_executor_job(
partial(
transmission_rpc.Client,
username=username,
password=password,
host=host,
port=port,
)
)
_LOGGER.debug("Successfully connected to %s", host)
return api
except TransmissionAuthError as error:
_LOGGER.error("Credentials for Transmission client are not valid")
raise AuthenticationError from error
except TransmissionConnectError as error:
_LOGGER.error("Connecting to the Transmission client %s failed", host)
raise CannotConnect from error
except TransmissionError as error:
_LOGGER.error(error)
raise UnknownError from error