New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Convert qbittorrent to config flow #45618
Changes from all commits
82b9467
c20c9d1
f06a65d
f4ebb14
d763c99
33ff29a
1d49571
b97ec4e
e0ca532
3a30993
d066afb
3d357ae
71a2010
7d3ddd7
912a8cc
aad410f
26aea2b
a97ac11
7bd7ff9
99a0c8d
fbfaf71
9d15866
fcab3b3
c372bac
65907f0
5bbfe42
35bde58
b7c78f8
52c0bfe
b61fa70
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1 +1,85 @@ | ||||||||||||||||||||||||||||||||
"""The qbittorrent component.""" | ||||||||||||||||||||||||||||||||
import asyncio | ||||||||||||||||||||||||||||||||
import logging | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
from qbittorrent.client import LoginRequired | ||||||||||||||||||||||||||||||||
from requests.exceptions import RequestException | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry | ||||||||||||||||||||||||||||||||
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME | ||||||||||||||||||||||||||||||||
from homeassistant.core import HomeAssistant | ||||||||||||||||||||||||||||||||
from homeassistant.exceptions import ConfigEntryNotReady | ||||||||||||||||||||||||||||||||
from homeassistant.helpers import config_per_platform | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
from .client import create_client | ||||||||||||||||||||||||||||||||
from .const import DATA_KEY_CLIENT, DATA_KEY_NAME, DOMAIN | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
PLATFORMS = ["sensor"] | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
_LOGGER = logging.getLogger(__name__) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
async def async_setup(hass: HomeAssistant, config: dict): | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this logic is done inside sensor.py, you can removed the part of needing the handle the config per platform and trigger the import in (Which is also a nice place to throw a deprecation warning of the YAML configuration being deprecated). |
||||||||||||||||||||||||||||||||
"""Set up the Qbittorrent component.""" | ||||||||||||||||||||||||||||||||
hass.data.setdefault(DOMAIN, {}) | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't used here, and should move to |
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
# Import configuration from sensor platform | ||||||||||||||||||||||||||||||||
config_platform = config_per_platform(config, "sensor") | ||||||||||||||||||||||||||||||||
for p_type, p_config in config_platform: | ||||||||||||||||||||||||||||||||
if p_type != DOMAIN: | ||||||||||||||||||||||||||||||||
continue | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
hass.async_create_task( | ||||||||||||||||||||||||||||||||
hass.config_entries.flow.async_init( | ||||||||||||||||||||||||||||||||
DOMAIN, | ||||||||||||||||||||||||||||||||
context={"source": SOURCE_IMPORT}, | ||||||||||||||||||||||||||||||||
data=p_config, | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
return True | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||
"""Set up Qbittorrent from a config entry.""" | ||||||||||||||||||||||||||||||||
name = "Qbittorrent" | ||||||||||||||||||||||||||||||||
try: | ||||||||||||||||||||||||||||||||
client = await hass.async_add_executor_job( | ||||||||||||||||||||||||||||||||
create_client, | ||||||||||||||||||||||||||||||||
entry.data[CONF_URL], | ||||||||||||||||||||||||||||||||
entry.data[CONF_USERNAME], | ||||||||||||||||||||||||||||||||
entry.data[CONF_PASSWORD], | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
except LoginRequired: | ||||||||||||||||||||||||||||||||
_LOGGER.error("Invalid authentication") | ||||||||||||||||||||||||||||||||
return False | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
except RequestException as err: | ||||||||||||||||||||||||||||||||
_LOGGER.error("Connection failed") | ||||||||||||||||||||||||||||||||
raise ConfigEntryNotReady from err | ||||||||||||||||||||||||||||||||
Comment on lines
+58
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
hass.data[DOMAIN][entry.data[CONF_URL]] = { | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use the entry_id as the index
Suggested change
|
||||||||||||||||||||||||||||||||
DATA_KEY_CLIENT: client, | ||||||||||||||||||||||||||||||||
DATA_KEY_NAME: name, | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
for platform in PLATFORMS: | ||||||||||||||||||||||||||||||||
hass.async_create_task( | ||||||||||||||||||||||||||||||||
hass.config_entries.async_forward_entry_setup(entry, platform) | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
Comment on lines
+65
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||
return True | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||
"""Unload Qbittorrent Entry from config_entry.""" | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
unload_ok = all( | ||||||||||||||||||||||||||||||||
await asyncio.gather( | ||||||||||||||||||||||||||||||||
*( | ||||||||||||||||||||||||||||||||
hass.config_entries.async_forward_entry_unload(config_entry, platform) | ||||||||||||||||||||||||||||||||
for platform in PLATFORMS | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
return unload_ok | ||||||||||||||||||||||||||||||||
Comment on lines
+75
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
"""Some common functions used in the qbittorrent components.""" | ||
from qbittorrent.client import Client | ||
|
||
|
||
def get_main_data_client(client: Client): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing return type |
||
"""Get the main data from the Qtorrent client.""" | ||
return client.sync_main_data() | ||
Comment on lines
+5
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What would be the benefit of having this in a separate method? |
||
|
||
|
||
def create_client(url, username, password): | ||
"""Create the Qtorrent client.""" | ||
client = Client(url) | ||
client.login(username, password) | ||
return client | ||
Comment on lines
+10
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is used in a single place. I think it would make the code more readable if this was just moved into the |
||
|
||
|
||
def retrieve_torrentdata(client: Client, torrentfilter): | ||
"""Retrieve torrent data from the Qtorrent client with specific filters.""" | ||
return client.torrents(filter=torrentfilter) | ||
Comment on lines
+17
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a proxy method, is this really useful? It is also partly typed, please type methods fully. |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,116 @@ | ||||||
"""Config flow for qBittorrent integration.""" | ||||||
import logging | ||||||
|
||||||
from qbittorrent.client import LoginRequired | ||||||
from requests.exceptions import RequestException | ||||||
import voluptuous as vol | ||||||
|
||||||
from homeassistant import config_entries | ||||||
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME | ||||||
|
||||||
from .client import create_client | ||||||
from .const import DOMAIN # pylint:disable=unused-import | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unneeded disable
Suggested change
|
||||||
|
||||||
_LOGGER = logging.getLogger(__name__) | ||||||
|
||||||
|
||||||
async def validate_input(hass, data): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we type this? |
||||||
"""Validate user or import input.""" | ||||||
errors = {} | ||||||
try: | ||||||
await hass.async_add_executor_job( | ||||||
create_client, | ||||||
data[CONF_URL], | ||||||
data[CONF_USERNAME], | ||||||
data[CONF_PASSWORD], | ||||||
) | ||||||
except LoginRequired: | ||||||
errors["base"] = "invalid_auth" | ||||||
|
||||||
except RequestException as err: | ||||||
errors["base"] = "cannot_connect" | ||||||
_LOGGER.error("Connection failed - %s", err) | ||||||
return errors | ||||||
|
||||||
|
||||||
class QBittorrentConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): | ||||||
"""Handle a config flow for qbittorrent.""" | ||||||
|
||||||
VERSION = 1 | ||||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No longer used
Suggested change
|
||||||
|
||||||
def __init__(self): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
"""Initialize qbittorrent config flow.""" | ||||||
self._data = None | ||||||
self._title = None | ||||||
|
||||||
async def async_step_import(self, device_config): | ||||||
"""Import a configuration.yaml config.""" | ||||||
data = {} | ||||||
|
||||||
data[CONF_URL] = device_config.get(CONF_URL) | ||||||
data[CONF_USERNAME] = device_config.get(CONF_USERNAME) | ||||||
data[CONF_PASSWORD] = device_config.get(CONF_PASSWORD) | ||||||
|
||||||
errors = await validate_input(self.hass, data) | ||||||
if not errors: | ||||||
self._data = data | ||||||
self._title = data[CONF_URL] | ||||||
return await self.async_step_import_confirm() | ||||||
geoffreylagaisse marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
return self.async_abort(reason=errors["base"]) | ||||||
|
||||||
async def async_step_import_confirm(self, user_input=None): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't use a confirmation step; instead, just import that data and migrate the user. |
||||||
"""Confirm the user wants to import the config entry.""" | ||||||
if user_input is None: | ||||||
return self.async_show_form( | ||||||
step_id="import_confirm", | ||||||
description_placeholders={"id": self._title}, | ||||||
) | ||||||
|
||||||
await self.async_set_unique_id(self._data[CONF_URL]) | ||||||
self._abort_if_unique_id_configured() | ||||||
|
||||||
return self.async_create_entry( | ||||||
title=self._data[CONF_URL], | ||||||
data={ | ||||||
CONF_USERNAME: self._data[CONF_USERNAME], | ||||||
CONF_PASSWORD: self._data[CONF_PASSWORD], | ||||||
CONF_URL: self._data[CONF_URL], | ||||||
}, | ||||||
) | ||||||
|
||||||
async def async_step_user(self, user_input=None): | ||||||
"""Handle the initial step.""" | ||||||
errors = {} | ||||||
if user_input is not None: | ||||||
await self.async_set_unique_id(user_input[CONF_URL]) | ||||||
self._abort_if_unique_id_configured() | ||||||
|
||||||
_LOGGER.debug( | ||||||
"Configuring user: %s - Password hidden", user_input[CONF_USERNAME] | ||||||
) | ||||||
|
||||||
errors = await validate_input(self.hass, user_input) | ||||||
|
||||||
if not errors: | ||||||
return self.async_create_entry( | ||||||
title=user_input[CONF_URL], | ||||||
data={ | ||||||
CONF_USERNAME: user_input[CONF_USERNAME], | ||||||
CONF_PASSWORD: user_input[CONF_PASSWORD], | ||||||
CONF_URL: user_input[CONF_URL], | ||||||
}, | ||||||
) | ||||||
|
||||||
return self.async_show_form( | ||||||
step_id="user", | ||||||
data_schema=vol.Schema( | ||||||
{ | ||||||
vol.Required(CONF_URL): str, | ||||||
vol.Required(CONF_USERNAME): str, | ||||||
vol.Required(CONF_PASSWORD): str, | ||||||
} | ||||||
), | ||||||
errors=errors, | ||||||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
"""Added constants for the qbittorrent component.""" | ||
|
||
SENSOR_TYPE_CURRENT_STATUS = "current_status" | ||
SENSOR_TYPE_DOWNLOAD_SPEED = "download_speed" | ||
SENSOR_TYPE_UPLOAD_SPEED = "upload_speed" | ||
SENSOR_TYPE_TOTAL_TORRENTS = "total_torrents" | ||
SENSOR_TYPE_ACTIVE_TORRENTS = "active_torrents" | ||
SENSOR_TYPE_INACTIVE_TORRENTS = "inactive_torrents" | ||
SENSOR_TYPE_DOWNLOADING_TORRENTS = "downloading_torrents" | ||
SENSOR_TYPE_SEEDING_TORRENTS = "seeding_torrents" | ||
SENSOR_TYPE_RESUMED_TORRENTS = "resumed_torrents" | ||
SENSOR_TYPE_PAUSED_TORRENTS = "paused_torrents" | ||
SENSOR_TYPE_COMPLETED_TORRENTS = "completed_torrents" | ||
|
||
DEFAULT_NAME = "qbittorrent" | ||
TRIM_SIZE = 35 | ||
CONF_CATEGORIES = "categories" | ||
|
||
DOMAIN = DEFAULT_NAME | ||
|
||
DATA_KEY_CLIENT = "client" | ||
DATA_KEY_NAME = "name" | ||
|
||
SERVICE_ADD_DOWNLOAD = "add_download" | ||
SERVICE_REMOVE_DOWNLOAD = "remove_download" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.