Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Copy link

Copilot AI Dec 16, 2025

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.
Copy link
Copy Markdown
Contributor Author

@andrew-codechimp andrew-codechimp Dec 16, 2025

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": "Get a list of current torrents",
"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