mirror of
https://github.com/home-assistant/core.git
synced 2026-04-17 23:53:49 +01:00
Add event entity to Transmission (#166686)
This commit is contained in:
@@ -40,7 +40,7 @@ from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.EVENT, Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
MIGRATION_NAME_TO_KEY = {
|
||||
# Sensors
|
||||
|
||||
@@ -55,6 +55,10 @@ EVENT_STARTED_TORRENT = "transmission_started_torrent"
|
||||
EVENT_REMOVED_TORRENT = "transmission_removed_torrent"
|
||||
EVENT_DOWNLOADED_TORRENT = "transmission_downloaded_torrent"
|
||||
|
||||
EVENT_TYPE_STARTED = "started"
|
||||
EVENT_TYPE_REMOVED = "removed"
|
||||
EVENT_TYPE_DOWNLOADED = "downloaded"
|
||||
|
||||
STATE_UP_DOWN = "up_down"
|
||||
STATE_SEEDING = "seeding"
|
||||
STATE_DOWNLOADING = "downloading"
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
"""Coordinator for transmssion integration."""
|
||||
"""Coordinator for transmission integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
import transmission_rpc
|
||||
from transmission_rpc.session import SessionStats
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import ATTR_ID, ATTR_NAME, CONF_HOST
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
ATTR_DOWNLOAD_PATH,
|
||||
ATTR_LABELS,
|
||||
CONF_LIMIT,
|
||||
CONF_ORDER,
|
||||
DEFAULT_LIMIT,
|
||||
@@ -23,13 +28,28 @@ from .const import (
|
||||
EVENT_DOWNLOADED_TORRENT,
|
||||
EVENT_REMOVED_TORRENT,
|
||||
EVENT_STARTED_TORRENT,
|
||||
EVENT_TYPE_DOWNLOADED,
|
||||
EVENT_TYPE_REMOVED,
|
||||
EVENT_TYPE_STARTED,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type EventCallback = Callable[[TransmissionEventData], None]
|
||||
type TransmissionConfigEntry = ConfigEntry[TransmissionDataUpdateCoordinator]
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransmissionEventData:
|
||||
"""Data for a single event."""
|
||||
|
||||
event_type: str
|
||||
name: str
|
||||
id: int
|
||||
download_path: str
|
||||
labels: list[str]
|
||||
|
||||
|
||||
class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]):
|
||||
"""Transmission dataupdate coordinator class."""
|
||||
|
||||
@@ -49,6 +69,7 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]):
|
||||
self._all_torrents: list[transmission_rpc.Torrent] = []
|
||||
self._completed_torrents: list[transmission_rpc.Torrent] = []
|
||||
self._started_torrents: list[transmission_rpc.Torrent] = []
|
||||
self._event_listeners: dict[str, EventCallback] = {}
|
||||
self.torrents: list[transmission_rpc.Torrent] = []
|
||||
super().__init__(
|
||||
hass,
|
||||
@@ -68,9 +89,32 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]):
|
||||
"""Return order."""
|
||||
return self.config_entry.options.get(CONF_ORDER, DEFAULT_ORDER) # type: ignore[no-any-return]
|
||||
|
||||
@callback
|
||||
def async_add_event_listener(
|
||||
self, update_callback: EventCallback, target_event_id: str
|
||||
) -> Callable[[], None]:
|
||||
"""Listen for updates."""
|
||||
self._event_listeners[target_event_id] = update_callback
|
||||
return partial(self.__async_remove_listener_internal, target_event_id)
|
||||
|
||||
def __async_remove_listener_internal(self, listener_id: str) -> None:
|
||||
self._event_listeners.pop(listener_id, None)
|
||||
|
||||
@callback
|
||||
def _async_notify_event_listeners(self, event: TransmissionEventData) -> None:
|
||||
"""Notify event listeners in the event loop."""
|
||||
for listener in list(self._event_listeners.values()):
|
||||
listener(event)
|
||||
|
||||
async def _async_update_data(self) -> SessionStats:
|
||||
"""Update transmission data."""
|
||||
return await self.hass.async_add_executor_job(self.update)
|
||||
data = await self.hass.async_add_executor_job(self.update)
|
||||
|
||||
self.check_completed_torrent()
|
||||
self.check_started_torrent()
|
||||
self.check_removed_torrent()
|
||||
|
||||
return data
|
||||
|
||||
def update(self) -> SessionStats:
|
||||
"""Get the latest data from Transmission instance."""
|
||||
@@ -82,10 +126,6 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]):
|
||||
except transmission_rpc.TransmissionError as err:
|
||||
raise UpdateFailed("Unable to connect to Transmission client") from err
|
||||
|
||||
self.check_completed_torrent()
|
||||
self.check_started_torrent()
|
||||
self.check_removed_torrent()
|
||||
|
||||
return data
|
||||
|
||||
def init_torrent_list(self) -> None:
|
||||
@@ -108,15 +148,24 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]):
|
||||
|
||||
for torrent in current_completed_torrents:
|
||||
if torrent.id not in old_completed_torrents:
|
||||
self.hass.bus.fire(
|
||||
# Once event triggers are out of labs we can remove the bus event
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_DOWNLOADED_TORRENT,
|
||||
{
|
||||
"name": torrent.name,
|
||||
"id": torrent.id,
|
||||
"download_path": torrent.download_dir,
|
||||
"labels": torrent.labels,
|
||||
ATTR_NAME: torrent.name,
|
||||
ATTR_ID: torrent.id,
|
||||
ATTR_DOWNLOAD_PATH: torrent.download_dir,
|
||||
ATTR_LABELS: torrent.labels,
|
||||
},
|
||||
)
|
||||
event = TransmissionEventData(
|
||||
event_type=EVENT_TYPE_DOWNLOADED,
|
||||
name=torrent.name,
|
||||
id=torrent.id,
|
||||
download_path=torrent.download_dir or "",
|
||||
labels=torrent.labels,
|
||||
)
|
||||
self._async_notify_event_listeners(event)
|
||||
|
||||
self._completed_torrents = current_completed_torrents
|
||||
|
||||
@@ -130,15 +179,24 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]):
|
||||
|
||||
for torrent in current_started_torrents:
|
||||
if torrent.id not in old_started_torrents:
|
||||
self.hass.bus.fire(
|
||||
# Once event triggers are out of labs we can remove the bus event
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_STARTED_TORRENT,
|
||||
{
|
||||
"name": torrent.name,
|
||||
"id": torrent.id,
|
||||
"download_path": torrent.download_dir,
|
||||
"labels": torrent.labels,
|
||||
ATTR_NAME: torrent.name,
|
||||
ATTR_ID: torrent.id,
|
||||
ATTR_DOWNLOAD_PATH: torrent.download_dir,
|
||||
ATTR_LABELS: torrent.labels,
|
||||
},
|
||||
)
|
||||
event = TransmissionEventData(
|
||||
event_type=EVENT_TYPE_STARTED,
|
||||
name=torrent.name,
|
||||
id=torrent.id,
|
||||
download_path=torrent.download_dir or "",
|
||||
labels=torrent.labels,
|
||||
)
|
||||
self._async_notify_event_listeners(event)
|
||||
|
||||
self._started_torrents = current_started_torrents
|
||||
|
||||
@@ -148,15 +206,24 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]):
|
||||
|
||||
for torrent in self._all_torrents:
|
||||
if torrent.id not in current_torrents:
|
||||
self.hass.bus.fire(
|
||||
# Once event triggers are out of labs we can remove the bus event
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_REMOVED_TORRENT,
|
||||
{
|
||||
"name": torrent.name,
|
||||
"id": torrent.id,
|
||||
"download_path": torrent.download_dir,
|
||||
"labels": torrent.labels,
|
||||
ATTR_NAME: torrent.name,
|
||||
ATTR_ID: torrent.id,
|
||||
ATTR_DOWNLOAD_PATH: torrent.download_dir,
|
||||
ATTR_LABELS: torrent.labels,
|
||||
},
|
||||
)
|
||||
event = TransmissionEventData(
|
||||
event_type=EVENT_TYPE_REMOVED,
|
||||
name=torrent.name,
|
||||
id=torrent.id,
|
||||
download_path=torrent.download_dir or "",
|
||||
labels=torrent.labels,
|
||||
)
|
||||
self._async_notify_event_listeners(event)
|
||||
|
||||
self._all_torrents = self.torrents.copy()
|
||||
|
||||
|
||||
85
homeassistant/components/transmission/event.py
Normal file
85
homeassistant/components/transmission/event.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Define events for the Transmission integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components.event import EventEntity, EventEntityDescription
|
||||
from homeassistant.const import ATTR_ID, ATTR_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
ATTR_DOWNLOAD_PATH,
|
||||
ATTR_LABELS,
|
||||
EVENT_TYPE_DOWNLOADED,
|
||||
EVENT_TYPE_REMOVED,
|
||||
EVENT_TYPE_STARTED,
|
||||
)
|
||||
from .coordinator import TransmissionConfigEntry, TransmissionEventData
|
||||
from .entity import TransmissionEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: TransmissionConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Transmission event platform."""
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
description = EventEntityDescription(
|
||||
key="torrent",
|
||||
translation_key="torrent",
|
||||
event_types=[
|
||||
EVENT_TYPE_STARTED,
|
||||
EVENT_TYPE_DOWNLOADED,
|
||||
EVENT_TYPE_REMOVED,
|
||||
],
|
||||
)
|
||||
|
||||
async_add_entities([TransmissionEvent(coordinator, description)])
|
||||
|
||||
|
||||
class TransmissionEvent(TransmissionEntity, EventEntity):
|
||||
"""Representation of a Transmission event entity."""
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert self._attr_unique_id
|
||||
|
||||
self.async_on_remove(
|
||||
self.coordinator.async_add_event_listener(
|
||||
self._handle_event, self._attr_unique_id
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_event(self, event_data: TransmissionEventData) -> None:
|
||||
"""Handle the torrent events."""
|
||||
|
||||
event_type = event_data.event_type
|
||||
|
||||
if event_type not in self.event_types:
|
||||
_LOGGER.warning("Event type %s is not known", event_type)
|
||||
return
|
||||
|
||||
self._trigger_event(
|
||||
event_type,
|
||||
{
|
||||
ATTR_NAME: event_data.name,
|
||||
ATTR_ID: event_data.id,
|
||||
ATTR_DOWNLOAD_PATH: event_data.download_path,
|
||||
ATTR_LABELS: event_data.labels,
|
||||
},
|
||||
)
|
||||
|
||||
self.async_write_ha_state()
|
||||
@@ -8,6 +8,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"event": {
|
||||
"torrent": {
|
||||
"default": "mdi:folder-file-outline"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"active_torrents": {
|
||||
"default": "mdi:counter"
|
||||
|
||||
@@ -50,6 +50,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"event": {
|
||||
"torrent": {
|
||||
"name": "Torrent",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"downloaded": "Downloaded",
|
||||
"removed": "Removed",
|
||||
"started": "Started"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"active_torrents": {
|
||||
"name": "Active torrents",
|
||||
|
||||
102
tests/components/transmission/test_event.py
Normal file
102
tests/components/transmission/test_event.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""Tests for the Transmission event platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.event import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES
|
||||
from homeassistant.components.transmission.const import (
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
EVENT_TYPE_DOWNLOADED,
|
||||
EVENT_TYPE_REMOVED,
|
||||
EVENT_TYPE_STARTED,
|
||||
)
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_event_entity_setup(
|
||||
hass: HomeAssistant,
|
||||
mock_transmission_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the event entity is created with expected capabilities."""
|
||||
with patch("homeassistant.components.transmission.PLATFORMS", [Platform.EVENT]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("event.transmission_torrent")
|
||||
assert state is not None
|
||||
assert state.state == "unknown"
|
||||
assert state.attributes[ATTR_EVENT_TYPE] is None
|
||||
assert state.attributes[ATTR_EVENT_TYPES] == [
|
||||
EVENT_TYPE_STARTED,
|
||||
EVENT_TYPE_DOWNLOADED,
|
||||
EVENT_TYPE_REMOVED,
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("hass_event", "expected_event_type"),
|
||||
[
|
||||
(EVENT_TYPE_STARTED, EVENT_TYPE_STARTED),
|
||||
(EVENT_TYPE_DOWNLOADED, EVENT_TYPE_DOWNLOADED),
|
||||
(EVENT_TYPE_REMOVED, EVENT_TYPE_REMOVED),
|
||||
],
|
||||
)
|
||||
async def test_event_updates_state(
|
||||
hass: HomeAssistant,
|
||||
mock_transmission_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
hass_event: str,
|
||||
expected_event_type: str,
|
||||
) -> None:
|
||||
"""Test Transmission events update the entity state and attributes."""
|
||||
with patch("homeassistant.components.transmission.PLATFORMS", [Platform.EVENT]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = mock_transmission_client.return_value
|
||||
torrent_status = {
|
||||
EVENT_TYPE_STARTED: "downloading",
|
||||
EVENT_TYPE_DOWNLOADED: "seeding",
|
||||
EVENT_TYPE_REMOVED: "stopped",
|
||||
}[hass_event]
|
||||
torrent = SimpleNamespace(
|
||||
id=1,
|
||||
name="Test",
|
||||
status=torrent_status,
|
||||
download_dir="/downloads",
|
||||
labels=[],
|
||||
)
|
||||
|
||||
torrents_sequence = {
|
||||
EVENT_TYPE_STARTED: [[torrent]],
|
||||
EVENT_TYPE_DOWNLOADED: [[torrent]],
|
||||
EVENT_TYPE_REMOVED: [[torrent], []],
|
||||
}[hass_event]
|
||||
|
||||
client.get_torrents.side_effect = torrents_sequence
|
||||
|
||||
for _ in torrents_sequence:
|
||||
freezer.tick(timedelta(seconds=DEFAULT_SCAN_INTERVAL + 1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("event.transmission_torrent")
|
||||
assert state is not None
|
||||
assert state.attributes[ATTR_EVENT_TYPE] == expected_event_type
|
||||
assert state.attributes["id"] == 1
|
||||
assert state.attributes["name"] == "Test"
|
||||
assert state.attributes["download_path"] == "/downloads"
|
||||
assert state.attributes["labels"] == []
|
||||
assert dt_util.parse_datetime(state.state) is not None
|
||||
Reference in New Issue
Block a user