Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions homeassistant/components/transmission/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@

ATTR_DELETE_DATA = "delete_data"
ATTR_TORRENT = "torrent"
ATTR_TORRENTS = "torrents"
ATTR_DOWNLOAD_PATH = "download_path"
ATTR_TORRENT_FILTER = "torrent_filter"

SERVICE_ADD_TORRENT = "add_torrent"
SERVICE_GET_TORRENTS = "get_torrents"
SERVICE_REMOVE_TORRENT = "remove_torrent"
SERVICE_START_TORRENT = "start_torrent"
SERVICE_STOP_TORRENT = "stop_torrent"
Expand All @@ -54,3 +57,14 @@
STATE_UP_DOWN = "up_down"
STATE_SEEDING = "seeding"
STATE_DOWNLOADING = "downloading"

FILTER_MODES: dict[str, list[str] | None] = {
"started": ["downloading"],
"completed": ["seeding"],
"paused": ["stopped"],
"active": [
"seeding",
"downloading",
],
"all": None,
}
45 changes: 45 additions & 0 deletions homeassistant/components/transmission/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Helper functions for Transmission."""

from typing import Any

from transmission_rpc.torrent import Torrent


def format_torrent(torrent: Torrent) -> dict[str, Any]:
"""Format a single torrent."""
value: dict[str, Any] = {}

value["id"] = torrent.id
value["name"] = torrent.name
value["status"] = torrent.status.value
value["percent_done"] = f"{torrent.percent_done * 100:.2f}%"
value["ratio"] = f"{torrent.ratio:.2f}"
value["eta"] = str(torrent.eta) if torrent.eta else None
value["added_date"] = torrent.added_date.isoformat()
value["done_date"] = torrent.done_date.isoformat() if torrent.done_date else None
value["download_dir"] = torrent.download_dir
value["labels"] = torrent.labels

return value
Comment on lines +8 to +23

Copilot AI Dec 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function lacks a docstring explaining the purpose of formatting, the structure of the returned dictionary, or the meaning of fields like 'status.value'. Consider adding a detailed docstring that describes the returned data structure and field types.

Copilot uses AI. Check for mistakes.

@andrew-codechimp andrew-codechimp Dec 16, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure a docstring would add much.



def filter_torrents(
torrents: list[Torrent], statuses: list[str] | None = None
) -> list[Torrent]:
"""Filter torrents based on the statuses provided."""
return [
torrent
for torrent in torrents
if statuses is None or torrent.status in statuses
]


def format_torrents(
torrents: list[Torrent],
) -> dict[str, dict[str, Any]]:
"""Format a list of torrents."""
value = {}
for torrent in torrents:
value[torrent.name] = format_torrent(torrent)

return value
3 changes: 3 additions & 0 deletions homeassistant/components/transmission/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
"add_torrent": {
"service": "mdi:download"
},
"get_torrents": {
"service": "mdi:file-arrow-up-down-outline"
},
"remove_torrent": {
"service": "mdi:download-off"
},
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/transmission/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"domain": "transmission",
"name": "Transmission",
"codeowners": ["@engrbm87", "@JPHutchins"],
"codeowners": ["@engrbm87", "@JPHutchins", "@andrew-codechimp"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/transmission",
"integration_type": "service",
Expand Down
41 changes: 10 additions & 31 deletions homeassistant/components/transmission/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
from dataclasses import dataclass
from typing import Any

from transmission_rpc.torrent import Torrent

from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
Expand All @@ -20,6 +18,7 @@
from homeassistant.helpers.typing import StateType

from .const import (
FILTER_MODES,
STATE_ATTR_TORRENT_INFO,
STATE_DOWNLOADING,
STATE_SEEDING,
Expand All @@ -28,20 +27,10 @@
)
from .coordinator import TransmissionConfigEntry, TransmissionDataUpdateCoordinator
from .entity import TransmissionEntity
from .helpers import filter_torrents

PARALLEL_UPDATES = 0

MODES: dict[str, list[str] | None] = {
"started_torrents": ["downloading"],
"completed_torrents": ["seeding"],
"paused_torrents": ["stopped"],
"active_torrents": [
"seeding",
"downloading",
],
"total_torrents": None,
}


@dataclass(frozen=True, kw_only=True)
class TransmissionSensorEntityDescription(SensorEntityDescription):
Expand Down Expand Up @@ -84,43 +73,43 @@ class TransmissionSensorEntityDescription(SensorEntityDescription):
translation_key="active_torrents",
val_func=lambda coordinator: coordinator.data.active_torrent_count,
extra_state_attr_func=lambda coordinator: _torrents_info_attr(
coordinator=coordinator, key="active_torrents"
coordinator=coordinator, key="active"
),
),
TransmissionSensorEntityDescription(
key="paused_torrents",
translation_key="paused_torrents",
val_func=lambda coordinator: coordinator.data.paused_torrent_count,
extra_state_attr_func=lambda coordinator: _torrents_info_attr(
coordinator=coordinator, key="paused_torrents"
coordinator=coordinator, key="paused"
),
),
TransmissionSensorEntityDescription(
key="total_torrents",
translation_key="total_torrents",
val_func=lambda coordinator: coordinator.data.torrent_count,
extra_state_attr_func=lambda coordinator: _torrents_info_attr(
coordinator=coordinator, key="total_torrents"
coordinator=coordinator, key="total"
),
),
TransmissionSensorEntityDescription(
key="completed_torrents",
translation_key="completed_torrents",
val_func=lambda coordinator: len(
_filter_torrents(coordinator.torrents, MODES["completed_torrents"])
filter_torrents(coordinator.torrents, FILTER_MODES["completed"])
),
extra_state_attr_func=lambda coordinator: _torrents_info_attr(
coordinator=coordinator, key="completed_torrents"
coordinator=coordinator, key="completed"
),
),
TransmissionSensorEntityDescription(
key="started_torrents",
translation_key="started_torrents",
val_func=lambda coordinator: len(
_filter_torrents(coordinator.torrents, MODES["started_torrents"])
filter_torrents(coordinator.torrents, FILTER_MODES["started"])
),
extra_state_attr_func=lambda coordinator: _torrents_info_attr(
coordinator=coordinator, key="started_torrents"
coordinator=coordinator, key="started"
),
),
)
Expand Down Expand Up @@ -169,21 +158,11 @@ def get_state(upload: int, download: int) -> str:
return STATE_IDLE


def _filter_torrents(
torrents: list[Torrent], statuses: list[str] | None = None
) -> list[Torrent]:
return [
torrent
for torrent in torrents
if statuses is None or torrent.status in statuses
]


def _torrents_info_attr(
coordinator: TransmissionDataUpdateCoordinator, key: str
) -> dict[str, Any]:
infos = {}
torrents = _filter_torrents(coordinator.torrents, MODES[key])
torrents = filter_torrents(coordinator.torrents, FILTER_MODES.get(key))
torrents = SUPPORTED_ORDER_MODES[coordinator.order](torrents)
for torrent in torrents[: coordinator.limit]:
info = infos[torrent.name] = {
Expand Down
58 changes: 56 additions & 2 deletions homeassistant/components/transmission/services.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,51 @@
"""Define services for the Transmission integration."""

from enum import StrEnum
from functools import partial
import logging
from typing import cast
from typing import Any, cast

from transmission_rpc import Torrent
import voluptuous as vol

from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv, selector

from .const import (
ATTR_DELETE_DATA,
ATTR_DOWNLOAD_PATH,
ATTR_TORRENT,
ATTR_TORRENT_FILTER,
ATTR_TORRENTS,
CONF_ENTRY_ID,
DEFAULT_DELETE_DATA,
DOMAIN,
FILTER_MODES,
SERVICE_ADD_TORRENT,
SERVICE_GET_TORRENTS,
SERVICE_REMOVE_TORRENT,
SERVICE_START_TORRENT,
SERVICE_STOP_TORRENT,
)
from .coordinator import TransmissionDataUpdateCoordinator
from .helpers import filter_torrents, format_torrents

_LOGGER = logging.getLogger(__name__)


class TorrentFilter(StrEnum):
"""TorrentFilter model."""

ALL = "all"
STARTED = "started"
COMPLETED = "completed"
PAUSED = "paused"
ACTIVE = "active"


SERVICE_BASE_SCHEMA = vol.Schema(
{
vol.Required(CONF_ENTRY_ID): selector.ConfigEntrySelector(
Expand All @@ -45,6 +63,16 @@
),
)

SERVICE_GET_TORRENTS_SCHEMA = vol.All(
SERVICE_BASE_SCHEMA.extend(
{
vol.Required(ATTR_TORRENT_FILTER): vol.In(
[x.lower() for x in TorrentFilter]
),
}
),
)

SERVICE_REMOVE_TORRENT_SCHEMA = vol.All(
SERVICE_BASE_SCHEMA.extend(
{
Expand Down Expand Up @@ -111,6 +139,24 @@ async def _async_add_torrent(service: ServiceCall) -> None:
await coordinator.async_request_refresh()


async def _async_get_torrents(service: ServiceCall) -> dict[str, Any] | None:
"""Get torrents."""
coordinator = _get_coordinator_from_service_data(service)
torrent_filter: str = service.data[ATTR_TORRENT_FILTER]

def get_filtered_torrents() -> list[Torrent]:
"""Filter torrents based on the filter provided."""
all_torrents = coordinator.api.get_torrents()
return filter_torrents(all_torrents, FILTER_MODES[torrent_filter])

torrents = await service.hass.async_add_executor_job(get_filtered_torrents)

info = format_torrents(torrents)
return {
ATTR_TORRENTS: info,
}


async def _async_start_torrent(service: ServiceCall) -> None:
"""Start torrent."""
coordinator = _get_coordinator_from_service_data(service)
Expand Down Expand Up @@ -149,6 +195,14 @@ def async_setup_services(hass: HomeAssistant) -> None:
schema=SERVICE_ADD_TORRENT_SCHEMA,
)

hass.services.async_register(
DOMAIN,
SERVICE_GET_TORRENTS,
_async_get_torrents,
schema=SERVICE_GET_TORRENTS_SCHEMA,
supports_response=SupportsResponse.ONLY,
)

hass.services.async_register(
DOMAIN,
SERVICE_REMOVE_TORRENT,
Expand Down
21 changes: 21 additions & 0 deletions homeassistant/components/transmission/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,27 @@ add_torrent:
selector:
text:

get_torrents:
fields:
entry_id:
required: true
selector:
config_entry:
integration: transmission
torrent_filter:
required: true
example: "all"
default: "all"
selector:
select:
options:
- "all"
- "active"
- "started"
- "paused"
- "completed"
translation_key: torrent_filter

remove_torrent:
fields:
entry_id:
Expand Down
23 changes: 23 additions & 0 deletions homeassistant/components/transmission/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@
"oldest_first": "Oldest first",
"worst_ratio_first": "Worst ratio first"
}
},
"torrent_filter": {
"options": {
"active": "Active",
"all": "All",
"completed": "Completed",
"paused": "Paused",
"started": "Started"
}
}
},
"services": {
Expand All @@ -141,6 +150,20 @@
},
"name": "Add torrent"
},
"get_torrents": {
"description": "Gets a list of current torrents",
Comment thread
andrew-codechimp marked this conversation as resolved.
Outdated
"fields": {
"entry_id": {
"description": "[%key:component::transmission::services::add_torrent::fields::entry_id::description%]",
"name": "[%key:component::transmission::services::add_torrent::fields::entry_id::name%]"
},
"torrent_filter": {
"description": "What kind of torrents you want to return, such as All or Active.",
"name": "Torrent filter"
}
},
"name": "Get torrents"
},
"remove_torrent": {
"description": "Removes a torrent.",
"fields": {
Expand Down
Loading
Loading