mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 00:37:53 +00:00
Add config flow to transmission (#26434)
* Add config flow to transmission * Reworked code to add all sensors and switches * applied fixes * final touches * Add tests * fixed tests * fix get_api errors and entities availabilty update * update config_flows.py * fix pylint error * update .coveragerc * add codeowner * add test_options * fixed test_options
This commit is contained in:
parent
9b204ad162
commit
82b77c2d29
@ -675,7 +675,11 @@ omit =
|
||||
homeassistant/components/tradfri/cover.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
|
||||
|
@ -288,6 +288,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
|
||||
|
40
homeassistant/components/transmission/.translations/en.json
Normal file
40
homeassistant/components/transmission/.translations/en.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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})
|
||||
|
||||
@ -55,13 +46,9 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
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)]),
|
||||
}
|
||||
)
|
||||
},
|
||||
@ -69,70 +56,165 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""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]
|
||||
|
||||
import transmissionrpc
|
||||
from transmissionrpc.error import TransmissionError
|
||||
|
||||
try:
|
||||
api = transmissionrpc.Client(host, port=port, user=username, password=password)
|
||||
api.session_stats()
|
||||
except TransmissionError as error:
|
||||
if str(error).find("401: Unauthorized"):
|
||||
_LOGGER.error("Credentials for" " Transmission client are not valid")
|
||||
return False
|
||||
|
||||
tm_data = hass.data[DATA_TRANSMISSION] = TransmissionData(hass, config, api)
|
||||
|
||||
tm_data.update()
|
||||
tm_data.init_torrent_list()
|
||||
|
||||
def refresh(event_time):
|
||||
"""Get the latest data from Transmission."""
|
||||
tm_data.update()
|
||||
|
||||
track_time_interval(hass, refresh, 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"
|
||||
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]
|
||||
)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA
|
||||
)
|
||||
|
||||
sensorconfig = {
|
||||
"sensors": config[DOMAIN][CONF_MONITORED_CONDITIONS],
|
||||
"client_name": config[DOMAIN][CONF_NAME],
|
||||
}
|
||||
|
||||
discovery.load_platform(hass, "sensor", DOMAIN, sensorconfig, config)
|
||||
|
||||
if config[DOMAIN][TURTLE_MODE]:
|
||||
discovery.load_platform(hass, "switch", DOMAIN, sensorconfig, config)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry):
|
||||
"""Set up the Transmission Component."""
|
||||
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
|
||||
|
||||
|
||||
async def get_api(hass, host, port, username=None, password=None):
|
||||
"""Get Transmission client."""
|
||||
try:
|
||||
api = await hass.async_add_executor_job(
|
||||
transmissionrpc.Client, host, port, username, password
|
||||
)
|
||||
return api
|
||||
|
||||
except TransmissionError as error:
|
||||
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]}
|
||||
|
||||
hass.config_entries.async_update_entry(config_entry, options=options)
|
||||
|
||||
|
||||
class TransmissionClient:
|
||||
"""Transmission Client Object."""
|
||||
|
||||
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)
|
||||
|
||||
for platform in ["sensor", "switch"]:
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_forward_entry_setup(
|
||||
self.config_entry, platform
|
||||
)
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
return True
|
||||
|
||||
def set_scan_interval(self, scan_interval):
|
||||
"""Update scan interval."""
|
||||
|
||||
def refresh(event_time):
|
||||
"""Get the latest data from Transmission."""
|
||||
self.tm_data.update()
|
||||
|
||||
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
|
||||
@ -140,12 +222,9 @@ class TransmissionData:
|
||||
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()
|
||||
@ -153,15 +232,15 @@ class TransmissionData:
|
||||
|
||||
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()
|
||||
@ -211,6 +290,15 @@ class TransmissionData:
|
||||
"""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)
|
||||
|
104
homeassistant/components/transmission/config_flow.py
Normal file
104
homeassistant/components/transmission/config_flow.py
Normal file
@ -0,0 +1,104 @@
|
||||
"""Config flow for Transmission Bittorent Client."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
|
||||
from . import get_api
|
||||
from .const import DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
from .errors import AuthenticationError, CannotConnect, UnknownError
|
||||
|
||||
|
||||
class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a UniFi config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
"""Get the options flow for this handler."""
|
||||
return TransmissionOptionsFlowHandler(config_entry)
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the Transmission flow."""
|
||||
self.config = {}
|
||||
self.errors = {}
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
if self.hass.config_entries.async_entries(DOMAIN):
|
||||
return self.async_abort(reason="one_instance_allowed")
|
||||
|
||||
if user_input is not None:
|
||||
|
||||
self.config[CONF_NAME] = user_input.pop(CONF_NAME)
|
||||
try:
|
||||
await get_api(self.hass, **user_input)
|
||||
self.config.update(user_input)
|
||||
if "options" not in self.config:
|
||||
self.config["options"] = {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}
|
||||
return self.async_create_entry(
|
||||
title=self.config[CONF_NAME], data=self.config
|
||||
)
|
||||
except AuthenticationError:
|
||||
self.errors[CONF_USERNAME] = "wrong_credentials"
|
||||
self.errors[CONF_PASSWORD] = "wrong_credentials"
|
||||
except (CannotConnect, UnknownError):
|
||||
self.errors["base"] = "cannot_connect"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME, default=DEFAULT_NAME): str,
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Optional(CONF_USERNAME): str,
|
||||
vol.Optional(CONF_PASSWORD): str,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
|
||||
}
|
||||
),
|
||||
errors=self.errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_config):
|
||||
"""Import from Transmission client config."""
|
||||
self.config["options"] = {
|
||||
CONF_SCAN_INTERVAL: import_config.pop(CONF_SCAN_INTERVAL).seconds
|
||||
}
|
||||
|
||||
return await self.async_step_user(user_input=import_config)
|
||||
|
||||
|
||||
class TransmissionOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle Transmission client options."""
|
||||
|
||||
def __init__(self, config_entry):
|
||||
"""Initialize Transmission options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage the Transmission options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
options = {
|
||||
vol.Optional(
|
||||
CONF_SCAN_INTERVAL,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_SCAN_INTERVAL,
|
||||
self.config_entry.data["options"][CONF_SCAN_INTERVAL],
|
||||
),
|
||||
): int
|
||||
}
|
||||
|
||||
return self.async_show_form(step_id="init", data_schema=vol.Schema(options))
|
24
homeassistant/components/transmission/const.py
Normal file
24
homeassistant/components/transmission/const.py
Normal file
@ -0,0 +1,24 @@
|
||||
"""Constants for the Transmission Bittorent Client component."""
|
||||
DOMAIN = "transmission"
|
||||
|
||||
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],
|
||||
}
|
||||
SWITCH_TYPES = {"on_off": "Switch", "turtle_mode": "Turtle Mode"}
|
||||
|
||||
DEFAULT_NAME = "Transmission"
|
||||
DEFAULT_PORT = 9091
|
||||
DEFAULT_SCAN_INTERVAL = 120
|
||||
|
||||
ATTR_TORRENT = "torrent"
|
||||
SERVICE_ADD_TORRENT = "add_torrent"
|
||||
|
||||
DATA_UPDATED = "transmission_data_updated"
|
||||
DATA_TRANSMISSION = "data_transmission"
|
14
homeassistant/components/transmission/errors.py
Normal file
14
homeassistant/components/transmission/errors.py
Normal file
@ -0,0 +1,14 @@
|
||||
"""Errors for the Transmission component."""
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
|
||||
class AuthenticationError(HomeAssistantError):
|
||||
"""Wrong Username or Password."""
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Unable to connect to client."""
|
||||
|
||||
|
||||
class UnknownError(HomeAssistantError):
|
||||
"""Unknown Error."""
|
@ -1,10 +1,13 @@
|
||||
{
|
||||
"domain": "transmission",
|
||||
"name": "Transmission",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/transmission",
|
||||
"requirements": [
|
||||
"transmissionrpc==0.11"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
}
|
||||
"codeowners": [
|
||||
"@engrbm87"
|
||||
]
|
||||
}
|
@ -1,32 +1,29 @@
|
||||
"""Support for monitoring the Transmission BitTorrent client API."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.const import STATE_IDLE
|
||||
from homeassistant.const import CONF_NAME, STATE_IDLE
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from . import DATA_TRANSMISSION, DATA_UPDATED, SENSOR_TYPES
|
||||
from .const import DATA_TRANSMISSION, DATA_UPDATED, DOMAIN, SENSOR_TYPES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "Transmission"
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=120)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the Transmission sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
"""Import config from configuration.yaml."""
|
||||
pass
|
||||
|
||||
transmission_api = hass.data[DATA_TRANSMISSION]
|
||||
monitored_variables = discovery_info["sensors"]
|
||||
name = discovery_info["client_name"]
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the Transmission sensors."""
|
||||
|
||||
transmission_api = hass.data[DOMAIN][DATA_TRANSMISSION]
|
||||
name = config_entry.data[CONF_NAME]
|
||||
|
||||
dev = []
|
||||
for sensor_type in monitored_variables:
|
||||
for sensor_type in SENSOR_TYPES:
|
||||
dev.append(
|
||||
TransmissionSensor(
|
||||
sensor_type,
|
||||
|
@ -3,4 +3,4 @@ add_torrent:
|
||||
fields:
|
||||
torrent:
|
||||
description: URL, magnet link or Base64 encoded file.
|
||||
example: http://releases.ubuntu.com/19.04/ubuntu-19.04-desktop-amd64.iso.torrent}
|
||||
example: http://releases.ubuntu.com/19.04/ubuntu-19.04-desktop-amd64.iso.torrent
|
||||
|
40
homeassistant/components/transmission/strings.json
Normal file
40
homeassistant/components/transmission/strings.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,43 +1,50 @@
|
||||
"""Support for setting the Transmission BitTorrent client Turtle Mode."""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
||||
from . import DATA_TRANSMISSION, DATA_UPDATED
|
||||
from .const import DATA_TRANSMISSION, DATA_UPDATED, DOMAIN, SWITCH_TYPES
|
||||
|
||||
_LOGGING = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "Transmission Turtle Mode"
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Import config from configuration.yaml."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the Transmission switch."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
component_name = DATA_TRANSMISSION
|
||||
transmission_api = hass.data[component_name]
|
||||
name = discovery_info["client_name"]
|
||||
transmission_api = hass.data[DOMAIN][DATA_TRANSMISSION]
|
||||
name = config_entry.data[CONF_NAME]
|
||||
|
||||
async_add_entities([TransmissionSwitch(transmission_api, name)], True)
|
||||
dev = []
|
||||
for switch_type, switch_name in SWITCH_TYPES.items():
|
||||
dev.append(TransmissionSwitch(switch_type, switch_name, transmission_api, name))
|
||||
|
||||
async_add_entities(dev, True)
|
||||
|
||||
|
||||
class TransmissionSwitch(ToggleEntity):
|
||||
"""Representation of a Transmission switch."""
|
||||
|
||||
def __init__(self, transmission_client, name):
|
||||
def __init__(self, switch_type, switch_name, transmission_api, name):
|
||||
"""Initialize the Transmission switch."""
|
||||
self._name = name
|
||||
self.transmission_client = transmission_client
|
||||
self._name = switch_name
|
||||
self.client_name = name
|
||||
self.type = switch_type
|
||||
self._transmission_api = transmission_api
|
||||
self._state = STATE_OFF
|
||||
self._data = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the switch."""
|
||||
return self._name
|
||||
return f"{self.client_name} {self._name}"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@ -54,15 +61,30 @@ class TransmissionSwitch(ToggleEntity):
|
||||
"""Return true if device is on."""
|
||||
return self._state == STATE_ON
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Could the device be accessed during the last update call."""
|
||||
return self._transmission_api.available
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
_LOGGING.debug("Turning Turtle Mode of Transmission on")
|
||||
self.transmission_client.set_alt_speed_enabled(True)
|
||||
if self.type == "on_off":
|
||||
_LOGGING.debug("Starting all torrents")
|
||||
self._transmission_api.start_torrents()
|
||||
elif self.type == "turtle_mode":
|
||||
_LOGGING.debug("Turning Turtle Mode of Transmission on")
|
||||
self._transmission_api.set_alt_speed_enabled(True)
|
||||
self._transmission_api.update()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
_LOGGING.debug("Turning Turtle Mode of Transmission off")
|
||||
self.transmission_client.set_alt_speed_enabled(False)
|
||||
if self.type == "on_off":
|
||||
_LOGGING.debug("Stoping all torrents")
|
||||
self._transmission_api.stop_torrents()
|
||||
if self.type == "turtle_mode":
|
||||
_LOGGING.debug("Turning Turtle Mode of Transmission off")
|
||||
self._transmission_api.set_alt_speed_enabled(False)
|
||||
self._transmission_api.update()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Handle entity which will be added."""
|
||||
@ -76,7 +98,14 @@ class TransmissionSwitch(ToggleEntity):
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from Transmission and updates the state."""
|
||||
active = self.transmission_client.get_alt_speed_enabled()
|
||||
active = None
|
||||
if self.type == "on_off":
|
||||
self._data = self._transmission_api.data
|
||||
if self._data:
|
||||
active = self._data.activeTorrentCount > 0
|
||||
|
||||
elif self.type == "turtle_mode":
|
||||
active = self._transmission_api.get_alt_speed_enabled()
|
||||
|
||||
if active is None:
|
||||
return
|
||||
|
@ -62,6 +62,7 @@ FLOWS = [
|
||||
"tplink",
|
||||
"traccar",
|
||||
"tradfri",
|
||||
"transmission",
|
||||
"twentemilieu",
|
||||
"twilio",
|
||||
"unifi",
|
||||
|
@ -422,6 +422,9 @@ statsd==3.2.1
|
||||
# homeassistant.components.toon
|
||||
toonapilib==3.2.4
|
||||
|
||||
# homeassistant.components.transmission
|
||||
transmissionrpc==0.11
|
||||
|
||||
# homeassistant.components.twentemilieu
|
||||
twentemilieu==0.1.0
|
||||
|
||||
|
@ -173,6 +173,7 @@ TEST_REQUIREMENTS = (
|
||||
"srpenergy",
|
||||
"statsd",
|
||||
"toonapilib",
|
||||
"transmissionrpc",
|
||||
"twentemilieu",
|
||||
"uvcclient",
|
||||
"vsure",
|
||||
|
1
tests/components/transmission/__init__.py
Normal file
1
tests/components/transmission/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for Transmission."""
|
245
tests/components/transmission/test_config_flow.py
Normal file
245
tests/components/transmission/test_config_flow.py
Normal file
@ -0,0 +1,245 @@
|
||||
"""Tests for Met.no config flow."""
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from transmissionrpc.error import TransmissionError
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.transmission import config_flow
|
||||
from homeassistant.components.transmission.const import (
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
NAME = "Transmission"
|
||||
HOST = "192.168.1.100"
|
||||
USERNAME = "username"
|
||||
PASSWORD = "password"
|
||||
PORT = 9091
|
||||
SCAN_INTERVAL = 10
|
||||
|
||||
|
||||
@pytest.fixture(name="api")
|
||||
def mock_transmission_api():
|
||||
"""Mock an api."""
|
||||
with patch("transmissionrpc.Client"):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="auth_error")
|
||||
def mock_api_authentication_error():
|
||||
"""Mock an api."""
|
||||
with patch(
|
||||
"transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized")
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="conn_error")
|
||||
def mock_api_connection_error():
|
||||
"""Mock an api."""
|
||||
with patch(
|
||||
"transmissionrpc.Client",
|
||||
side_effect=TransmissionError("111: Connection refused"),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="unknown_error")
|
||||
def mock_api_unknown_error():
|
||||
"""Mock an api."""
|
||||
with patch("transmissionrpc.Client", side_effect=TransmissionError):
|
||||
yield
|
||||
|
||||
|
||||
def init_config_flow(hass):
|
||||
"""Init a configuration flow."""
|
||||
flow = config_flow.TransmissionFlowHandler()
|
||||
flow.hass = hass
|
||||
return flow
|
||||
|
||||
|
||||
async def test_flow_works(hass, api):
|
||||
"""Test user config."""
|
||||
flow = init_config_flow(hass)
|
||||
|
||||
result = await flow.async_step_user()
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
# test with required fields only
|
||||
result = await flow.async_step_user(
|
||||
{CONF_NAME: NAME, CONF_HOST: HOST, CONF_PORT: PORT}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == NAME
|
||||
assert result["data"][CONF_NAME] == NAME
|
||||
assert result["data"][CONF_HOST] == HOST
|
||||
assert result["data"][CONF_PORT] == PORT
|
||||
assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL
|
||||
|
||||
# test with all provided
|
||||
result = await flow.async_step_user(
|
||||
{
|
||||
CONF_NAME: NAME,
|
||||
CONF_HOST: HOST,
|
||||
CONF_USERNAME: USERNAME,
|
||||
CONF_PASSWORD: PASSWORD,
|
||||
CONF_PORT: PORT,
|
||||
}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == NAME
|
||||
assert result["data"][CONF_NAME] == NAME
|
||||
assert result["data"][CONF_HOST] == HOST
|
||||
assert result["data"][CONF_USERNAME] == USERNAME
|
||||
assert result["data"][CONF_PASSWORD] == PASSWORD
|
||||
assert result["data"][CONF_PORT] == PORT
|
||||
assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL
|
||||
|
||||
|
||||
async def test_options(hass):
|
||||
"""Test updating options."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title=CONF_NAME,
|
||||
data={
|
||||
"name": DEFAULT_NAME,
|
||||
"host": HOST,
|
||||
"username": USERNAME,
|
||||
"password": PASSWORD,
|
||||
"port": DEFAULT_PORT,
|
||||
"options": {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL},
|
||||
},
|
||||
options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL},
|
||||
)
|
||||
flow = init_config_flow(hass)
|
||||
options_flow = flow.async_get_options_flow(entry)
|
||||
|
||||
result = await options_flow.async_step_init()
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "init"
|
||||
result = await options_flow.async_step_init({CONF_SCAN_INTERVAL: 10})
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["data"][CONF_SCAN_INTERVAL] == 10
|
||||
|
||||
|
||||
async def test_import(hass, api):
|
||||
"""Test import step."""
|
||||
flow = init_config_flow(hass)
|
||||
|
||||
# import with minimum fields only
|
||||
result = await flow.async_step_import(
|
||||
{
|
||||
CONF_NAME: DEFAULT_NAME,
|
||||
CONF_HOST: HOST,
|
||||
CONF_PORT: DEFAULT_PORT,
|
||||
CONF_SCAN_INTERVAL: timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||
}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == DEFAULT_NAME
|
||||
assert result["data"][CONF_NAME] == DEFAULT_NAME
|
||||
assert result["data"][CONF_HOST] == HOST
|
||||
assert result["data"][CONF_PORT] == DEFAULT_PORT
|
||||
assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL
|
||||
|
||||
# import with all
|
||||
result = await flow.async_step_import(
|
||||
{
|
||||
CONF_NAME: NAME,
|
||||
CONF_HOST: HOST,
|
||||
CONF_USERNAME: USERNAME,
|
||||
CONF_PASSWORD: PASSWORD,
|
||||
CONF_PORT: PORT,
|
||||
CONF_SCAN_INTERVAL: timedelta(seconds=SCAN_INTERVAL),
|
||||
}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == NAME
|
||||
assert result["data"][CONF_NAME] == NAME
|
||||
assert result["data"][CONF_HOST] == HOST
|
||||
assert result["data"][CONF_USERNAME] == USERNAME
|
||||
assert result["data"][CONF_PASSWORD] == PASSWORD
|
||||
assert result["data"][CONF_PORT] == PORT
|
||||
assert result["data"]["options"][CONF_SCAN_INTERVAL] == SCAN_INTERVAL
|
||||
|
||||
|
||||
async def test_integration_already_exists(hass, api):
|
||||
"""Test we only allow a single config flow."""
|
||||
MockConfigEntry(domain=DOMAIN).add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "user"}
|
||||
)
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "one_instance_allowed"
|
||||
|
||||
|
||||
async def test_error_on_wrong_credentials(hass, auth_error):
|
||||
"""Test with wrong credentials."""
|
||||
flow = init_config_flow(hass)
|
||||
|
||||
result = await flow.async_step_user(
|
||||
{
|
||||
CONF_NAME: NAME,
|
||||
CONF_HOST: HOST,
|
||||
CONF_USERNAME: USERNAME,
|
||||
CONF_PASSWORD: PASSWORD,
|
||||
CONF_PORT: PORT,
|
||||
}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {
|
||||
CONF_USERNAME: "wrong_credentials",
|
||||
CONF_PASSWORD: "wrong_credentials",
|
||||
}
|
||||
|
||||
|
||||
async def test_error_on_connection_failure(hass, conn_error):
|
||||
"""Test when connection to host fails."""
|
||||
flow = init_config_flow(hass)
|
||||
|
||||
result = await flow.async_step_user(
|
||||
{
|
||||
CONF_NAME: NAME,
|
||||
CONF_HOST: HOST,
|
||||
CONF_USERNAME: USERNAME,
|
||||
CONF_PASSWORD: PASSWORD,
|
||||
CONF_PORT: PORT,
|
||||
}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_error_on_unknwon_error(hass, unknown_error):
|
||||
"""Test when connection to host fails."""
|
||||
flow = init_config_flow(hass)
|
||||
|
||||
result = await flow.async_step_user(
|
||||
{
|
||||
CONF_NAME: NAME,
|
||||
CONF_HOST: HOST,
|
||||
CONF_USERNAME: USERNAME,
|
||||
CONF_PASSWORD: PASSWORD,
|
||||
CONF_PORT: PORT,
|
||||
}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
Loading…
x
Reference in New Issue
Block a user