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:
Rami Mosleh 2019-09-26 12:14:57 +03:00 committed by Martin Hjelmare
parent 9b204ad162
commit 82b77c2d29
17 changed files with 723 additions and 128 deletions

View File

@ -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

View File

@ -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

View 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"
}
}
}
}
}

View File

@ -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)

View 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))

View 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"

View 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."""

View File

@ -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"
]
}

View File

@ -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,

View File

@ -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

View 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"
}
}
}
}
}

View File

@ -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

View File

@ -62,6 +62,7 @@ FLOWS = [
"tplink",
"traccar",
"tradfri",
"transmission",
"twentemilieu",
"twilio",
"unifi",

View File

@ -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

View File

@ -173,6 +173,7 @@ TEST_REQUIREMENTS = (
"srpenergy",
"statsd",
"toonapilib",
"transmissionrpc",
"twentemilieu",
"uvcclient",
"vsure",

View File

@ -0,0 +1 @@
"""Tests for Transmission."""

View 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"}