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
6 changes: 5 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,11 @@ omit =
homeassistant/components/tradfri/light.py
homeassistant/components/trafikverket_train/sensor.py
homeassistant/components/trafikverket_weatherstation/sensor.py
homeassistant/components/transmission/*
homeassistant/components/transmission/__init__.py
homeassistant/components/transmission/sensor.py
homeassistant/components/transmission/switch.py
homeassistant/components/transmission/const.py
homeassistant/components/transmission/errors.py
homeassistant/components/travisci/sensor.py
homeassistant/components/tuya/*
homeassistant/components/twentemilieu/const.py
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ homeassistant/components/tplink/* @rytilahti
homeassistant/components/traccar/* @ludeeus
homeassistant/components/tradfri/* @ggravlingen
homeassistant/components/trafikverket_train/* @endor-force
homeassistant/components/transmission/* @engrbm87
homeassistant/components/tts/* @robbiet480
homeassistant/components/twentemilieu/* @frenck
homeassistant/components/twilio_call/* @robbiet480
Expand Down
40 changes: 40 additions & 0 deletions homeassistant/components/transmission/.translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"config": {
"title": "Transmission",
"step": {
"user": {
"title": "Setup Transmission Client",
"data": {
"name": "Name",
"host": "Host",
"username": "User name",
"password": "Password",
"port": "Port"
}
},
"options": {
"title": "Configure Options",
"data": {
"scan_interval": "Update frequency"
}
}
},
"error": {
"wrong_credentials": "Wrong username or password",
"cannot_connect": "Unable to Connect to host"
},
"abort": {
"one_instance_allowed": "Only a single instance is necessary."
}
},
"options": {
"step": {
"init": {
"description": "Configure options for Transmission",
"data": {
"scan_interval": "Update frequency"
}
}
}
}
}
244 changes: 166 additions & 78 deletions homeassistant/components/transmission/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,38 @@
from datetime import timedelta
import logging

import transmissionrpc
from transmissionrpc.error import TransmissionError
import voluptuous as vol

from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
CONF_HOST,
CONF_MONITORED_CONDITIONS,
CONF_NAME,
CONF_PASSWORD,
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_USERNAME,
)
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.event import async_track_time_interval

from .const import (
ATTR_TORRENT,
DATA_TRANSMISSION,
DATA_UPDATED,
DEFAULT_NAME,
DEFAULT_PORT,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
SERVICE_ADD_TORRENT,
)
from .errors import AuthenticationError, CannotConnect, UnknownError

_LOGGER = logging.getLogger(__name__)

DOMAIN = "transmission"
DATA_UPDATED = "transmission_data_updated"
DATA_TRANSMISSION = "data_transmission"

DEFAULT_NAME = "Transmission"
DEFAULT_PORT = 9091
TURTLE_MODE = "turtle_mode"

SENSOR_TYPES = {
"active_torrents": ["Active Torrents", None],
"current_status": ["Status", None],
"download_speed": ["Down Speed", "MB/s"],
"paused_torrents": ["Paused Torrents", None],
"total_torrents": ["Total Torrents", None],
"upload_speed": ["Up Speed", "MB/s"],
"completed_torrents": ["Completed Torrents", None],
"started_torrents": ["Started Torrents", None],
}

DEFAULT_SCAN_INTERVAL = timedelta(seconds=120)

ATTR_TORRENT = "torrent"

SERVICE_ADD_TORRENT = "add_torrent"

SERVICE_ADD_TORRENT_SCHEMA = vol.Schema({vol.Required(ATTR_TORRENT): cv.string})

Expand All @@ -55,113 +46,201 @@
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(TURTLE_MODE, default=False): cv.boolean,
vol.Optional(
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
): cv.time_period,
vol.Optional(
CONF_MONITORED_CONDITIONS, default=["current_status"]
): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
}
)
},
extra=vol.ALLOW_EXTRA,
)


def setup(hass, config):
async def async_setup(hass, config):
"""Import the Transmission Component from config."""
if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN]
)
)

return True


async def async_setup_entry(hass, config_entry):
"""Set up the Transmission Component."""
host = config[DOMAIN][CONF_HOST]
username = config[DOMAIN].get(CONF_USERNAME)
password = config[DOMAIN].get(CONF_PASSWORD)
port = config[DOMAIN][CONF_PORT]
scan_interval = config[DOMAIN][CONF_SCAN_INTERVAL]
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}

if not config_entry.options:
await async_populate_options(hass, config_entry)

client = TransmissionClient(hass, config_entry)
client_id = config_entry.entry_id
hass.data[DOMAIN][client_id] = client
if not await client.async_setup():
return False

return True


async def async_unload_entry(hass, entry):
"""Unload Transmission Entry from config_entry."""
hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT)
if hass.data[DOMAIN][entry.entry_id].unsub_timer:
hass.data[DOMAIN][entry.entry_id].unsub_timer()

for component in "sensor", "switch":
await hass.config_entries.async_forward_entry_unload(entry, component)

del hass.data[DOMAIN]

return True
Comment thread
engrbm87 marked this conversation as resolved.

import transmissionrpc
from transmissionrpc.error import TransmissionError

async def get_api(hass, host, port, username=None, password=None):
"""Get Transmission client."""
try:
api = transmissionrpc.Client(host, port=port, user=username, password=password)
api.session_stats()
api = await hass.async_add_executor_job(
transmissionrpc.Client, host, port, username, password
)
return api

except TransmissionError as error:
if str(error).find("401: Unauthorized"):
_LOGGER.error("Credentials for" " Transmission client are not valid")
return False
if "401: Unauthorized" in str(error):
_LOGGER.error("Credentials for Transmission client are not valid")
raise AuthenticationError
if "111: Connection refused" in str(error):
_LOGGER.error("Connecting to the Transmission client failed")
raise CannotConnect

_LOGGER.error(error)
raise UnknownError


async def async_populate_options(hass, config_entry):
"""Populate default options for Transmission Client."""
options = {CONF_SCAN_INTERVAL: config_entry.data["options"][CONF_SCAN_INTERVAL]}

tm_data = hass.data[DATA_TRANSMISSION] = TransmissionData(hass, config, api)
hass.config_entries.async_update_entry(config_entry, options=options)

tm_data.update()
tm_data.init_torrent_list()

def refresh(event_time):
"""Get the latest data from Transmission."""
tm_data.update()
class TransmissionClient:
"""Transmission Client Object."""

track_time_interval(hass, refresh, scan_interval)
def __init__(self, hass, config_entry):
"""Initialize the Transmission RPC API."""
self.hass = hass
self.config_entry = config_entry
self.scan_interval = self.config_entry.options[CONF_SCAN_INTERVAL]
self.tm_data = None
self.unsub_timer = None

async def async_setup(self):
"""Set up the Transmission client."""

config = {
CONF_HOST: self.config_entry.data[CONF_HOST],
CONF_PORT: self.config_entry.data[CONF_PORT],
CONF_USERNAME: self.config_entry.data.get(CONF_USERNAME),
CONF_PASSWORD: self.config_entry.data.get(CONF_PASSWORD),
}
try:
api = await get_api(self.hass, **config)
except CannotConnect:
raise ConfigEntryNotReady
except (AuthenticationError, UnknownError):
return False

self.tm_data = self.hass.data[DOMAIN][DATA_TRANSMISSION] = TransmissionData(
self.hass, self.config_entry, api
)

await self.hass.async_add_executor_job(self.tm_data.init_torrent_list)
await self.hass.async_add_executor_job(self.tm_data.update)
self.set_scan_interval(self.scan_interval)

def add_torrent(service):
"""Add new torrent to download."""
torrent = service.data[ATTR_TORRENT]
if torrent.startswith(
("http", "ftp:", "magnet:")
) or hass.config.is_allowed_path(torrent):
api.add_torrent(torrent)
else:
_LOGGER.warning(
"Could not add torrent: " "unsupported type or no permission"
for platform in ["sensor", "switch"]:
self.hass.async_create_task(
self.hass.config_entries.async_forward_entry_setup(
self.config_entry, platform
)
)

hass.services.register(
DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA
)
def add_torrent(service):
"""Add new torrent to download."""
torrent = service.data[ATTR_TORRENT]
if torrent.startswith(
("http", "ftp:", "magnet:")
) or self.hass.config.is_allowed_path(torrent):
api.add_torrent(torrent)
else:
_LOGGER.warning(
"Could not add torrent: unsupported type or no permission"
)

self.hass.services.async_register(
DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA
)

self.config_entry.add_update_listener(self.async_options_updated)

sensorconfig = {
"sensors": config[DOMAIN][CONF_MONITORED_CONDITIONS],
"client_name": config[DOMAIN][CONF_NAME],
}
return True

discovery.load_platform(hass, "sensor", DOMAIN, sensorconfig, config)
def set_scan_interval(self, scan_interval):
"""Update scan interval."""

if config[DOMAIN][TURTLE_MODE]:
discovery.load_platform(hass, "switch", DOMAIN, sensorconfig, config)
def refresh(event_time):
Comment thread
MartinHjelmare marked this conversation as resolved.
"""Get the latest data from Transmission."""
self.tm_data.update()

return True
if self.unsub_timer is not None:
self.unsub_timer()
self.unsub_timer = async_track_time_interval(
self.hass, refresh, timedelta(seconds=scan_interval)
)

@staticmethod
async def async_options_updated(hass, entry):
"""Triggered by config entry options updates."""
hass.data[DOMAIN][entry.entry_id].set_scan_interval(
entry.options[CONF_SCAN_INTERVAL]
)


class TransmissionData:
"""Get the latest data and update the states."""

def __init__(self, hass, config, api):
"""Initialize the Transmission RPC API."""
self.hass = hass
self.data = None
self.torrents = None
self.session = None
self.available = True
self._api = api
self.completed_torrents = []
self.started_torrents = []
self.hass = hass

def update(self):
"""Get the latest data from Transmission instance."""
from transmissionrpc.error import TransmissionError

try:
self.data = self._api.session_stats()
self.torrents = self._api.get_torrents()
self.session = self._api.get_session()

self.check_completed_torrent()
self.check_started_torrent()
_LOGGER.debug("Torrent Data Updated")

dispatcher_send(self.hass, DATA_UPDATED)

_LOGGER.debug("Torrent Data updated")
self.available = True
except TransmissionError:
self.available = False
_LOGGER.error("Unable to connect to Transmission client")

dispatcher_send(self.hass, DATA_UPDATED)

def init_torrent_list(self):
"""Initialize torrent lists."""
self.torrents = self._api.get_torrents()
Expand Down Expand Up @@ -211,6 +290,15 @@ def get_completed_torrent_count(self):
"""Get the number of completed torrents."""
return len(self.completed_torrents)

def start_torrents(self):
"""Start all torrents."""
self._api.start_all()

def stop_torrents(self):
"""Stop all active torrents."""
torrent_ids = [torrent.id for torrent in self.torrents]
self._api.stop_torrent(torrent_ids)

def set_alt_speed_enabled(self, is_enabled):
"""Set the alternative speed flag."""
self._api.set_session(alt_speed_enabled=is_enabled)
Expand Down
Loading