-
-
Notifications
You must be signed in to change notification settings - Fork 37.5k
Add config flow to qBittorrent #82560
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
Merged
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
f830234
qbittorrent: implement config_flow
chrisx8 406ab31
qbittorrent: add English translations
chrisx8 190cb01
qbittorrent: create sensors with config_flow
chrisx8 9d17737
qbittorrent: set unique_id and icon
chrisx8 cd480a6
qbittorrent: add tests for config_flow
chrisx8 95102e8
qbittorrent: detect duplicate config entries
chrisx8 fe5921c
qbittorrent: import YAML config
chrisx8 1625d4a
qbittorrent: update coveragerc
chrisx8 d43e01f
qbittorrent: delete translations file
chrisx8 c716e32
create `deprecated_yaml` issue in `setup_platform`
chrisx8 9e136a0
move qbittorrent test fixtures to conftest.py
chrisx8 14e0a25
improve code quality & remove wrong unique_id
chrisx8 dd9ca3c
keep PLATFORM_SCHEMA until YAML support is removed
chrisx8 6f1ab54
remove CONF_NAME in config entry, fix setup_entry
chrisx8 f0bbb7d
improve test suite
chrisx8 e0b559f
clean up QBittorrentSensor class
chrisx8 9546e2a
improve user flow tests
chrisx8 988a2c0
merge upstream/dev & resolve merge conflict
chrisx8 1066ffc
explicit result assertion & minor tweaks in tests
chrisx8 b949e31
implement entry unloading
chrisx8 46b54f4
add type hints
chrisx8 5985480
tweak config_flow data handling
chrisx8 ac36131
Merge remote-tracking branch 'upstream/dev' into qbittorrent
chrisx8 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,54 @@ | ||
| """The qbittorrent component.""" | ||
| import logging | ||
|
|
||
| from qbittorrent.client import LoginRequired | ||
| from requests.exceptions import RequestException | ||
|
|
||
| from homeassistant.config_entries import ConfigEntry | ||
| from homeassistant.const import ( | ||
| CONF_PASSWORD, | ||
| CONF_URL, | ||
| CONF_USERNAME, | ||
| CONF_VERIFY_SSL, | ||
| Platform, | ||
| ) | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.exceptions import ConfigEntryNotReady | ||
|
|
||
| from .const import DOMAIN | ||
| from .helpers import setup_client | ||
|
|
||
| PLATFORMS = [Platform.SENSOR] | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
| """Set up qBittorrent from a config entry.""" | ||
| hass.data.setdefault(DOMAIN, {}) | ||
| try: | ||
| hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job( | ||
| setup_client, | ||
| entry.data[CONF_URL], | ||
| entry.data[CONF_USERNAME], | ||
| entry.data[CONF_PASSWORD], | ||
| entry.data[CONF_VERIFY_SSL], | ||
| ) | ||
| except LoginRequired as err: | ||
| _LOGGER.error("Invalid credentials") | ||
|
MartinHjelmare marked this conversation as resolved.
|
||
| raise ConfigEntryNotReady from err | ||
|
Contributor
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. Time to implement reauth flow... |
||
| except RequestException as err: | ||
| _LOGGER.error("Failed to connect") | ||
| raise ConfigEntryNotReady from err | ||
|
|
||
| await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
| return True | ||
|
|
||
|
|
||
| async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
| """Unload qBittorrent config entry.""" | ||
| if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): | ||
| del hass.data[DOMAIN][entry.entry_id] | ||
| if not hass.data[DOMAIN]: | ||
| del hass.data[DOMAIN] | ||
| return unload_ok | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| """Config flow for qBittorrent.""" | ||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| from typing import Any | ||
|
|
||
| from qbittorrent.client import LoginRequired | ||
| from requests.exceptions import RequestException | ||
| import voluptuous as vol | ||
|
|
||
| from homeassistant.config_entries import ConfigFlow | ||
| from homeassistant.const import ( | ||
| CONF_NAME, | ||
| CONF_PASSWORD, | ||
| CONF_URL, | ||
| CONF_USERNAME, | ||
| CONF_VERIFY_SSL, | ||
| ) | ||
| from homeassistant.data_entry_flow import FlowResult | ||
|
|
||
| from .const import DEFAULT_NAME, DEFAULT_URL, DOMAIN | ||
| from .helpers import setup_client | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| USER_DATA_SCHEMA = vol.Schema( | ||
| { | ||
| vol.Required(CONF_URL, default=DEFAULT_URL): str, | ||
| vol.Required(CONF_USERNAME): str, | ||
| vol.Required(CONF_PASSWORD): str, | ||
| vol.Optional(CONF_VERIFY_SSL, default=True): bool, | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| class QbittorrentConfigFlow(ConfigFlow, domain=DOMAIN): | ||
| """Config flow for the qBittorrent integration.""" | ||
|
|
||
| async def async_step_user( | ||
| self, user_input: dict[str, Any] | None = None | ||
| ) -> FlowResult: | ||
| """Handle a user-initiated config flow.""" | ||
| errors = {} | ||
|
|
||
| if user_input is not None: | ||
| self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]}) | ||
| try: | ||
| await self.hass.async_add_executor_job( | ||
| setup_client, | ||
| user_input[CONF_URL], | ||
| user_input[CONF_USERNAME], | ||
| user_input[CONF_PASSWORD], | ||
| user_input[CONF_VERIFY_SSL], | ||
| ) | ||
| except LoginRequired: | ||
| errors = {"base": "invalid_auth"} | ||
| except RequestException: | ||
| errors = {"base": "cannot_connect"} | ||
| else: | ||
| return self.async_create_entry(title=DEFAULT_NAME, data=user_input) | ||
|
|
||
| schema = self.add_suggested_values_to_schema(USER_DATA_SCHEMA, user_input) | ||
| return self.async_show_form(step_id="user", data_schema=schema, errors=errors) | ||
|
|
||
| async def async_step_import(self, config: dict[str, Any]) -> FlowResult: | ||
| """Import a config entry from configuration.yaml.""" | ||
| self._async_abort_entries_match({CONF_URL: config[CONF_URL]}) | ||
| return self.async_create_entry( | ||
|
MartinHjelmare marked this conversation as resolved.
|
||
| title=config.get(CONF_NAME, DEFAULT_NAME), | ||
| data={ | ||
| CONF_URL: config[CONF_URL], | ||
| CONF_USERNAME: config[CONF_USERNAME], | ||
| CONF_PASSWORD: config[CONF_PASSWORD], | ||
| CONF_VERIFY_SSL: True, | ||
| }, | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,7 @@ | ||
| """Constants for qBittorrent.""" | ||
|
chrisx8 marked this conversation as resolved.
|
||
| from typing import Final | ||
|
|
||
| DOMAIN: Final = "qbittorrent" | ||
|
|
||
| DEFAULT_NAME = "qBittorrent" | ||
| DEFAULT_URL = "http://127.0.0.1:8080" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| """Helper functions for qBittorrent.""" | ||
| from qbittorrent.client import Client | ||
|
|
||
|
|
||
| def setup_client(url: str, username: str, password: str, verify_ssl: bool) -> Client: | ||
| """Create a qBittorrent client.""" | ||
| client = Client(url, verify=verify_ssl) | ||
| client.login(username, password) | ||
| # Get an arbitrary attribute to test if connection succeeds | ||
| client.get_alternative_speed_status() | ||
| return client |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ | |
| SensorEntityDescription, | ||
| SensorStateClass, | ||
| ) | ||
| from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry | ||
| from homeassistant.const import ( | ||
| CONF_NAME, | ||
| CONF_PASSWORD, | ||
|
|
@@ -23,12 +24,12 @@ | |
| UnitOfDataRate, | ||
| ) | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.exceptions import PlatformNotReady | ||
| from homeassistant.helpers import issue_registry as ir | ||
| import homeassistant.helpers.config_validation as cv | ||
| from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||
| from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType | ||
|
|
||
| from .const import DEFAULT_NAME | ||
| from .const import DEFAULT_NAME, DOMAIN | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
@@ -69,31 +70,41 @@ | |
| ) | ||
|
|
||
|
|
||
| def setup_platform( | ||
| async def async_setup_platform( | ||
| hass: HomeAssistant, | ||
| config: ConfigType, | ||
| add_entities: AddEntitiesCallback, | ||
| async_add_entities: AddEntitiesCallback, | ||
| discovery_info: DiscoveryInfoType | None = None, | ||
| ) -> None: | ||
| """Set up the qBittorrent sensors.""" | ||
|
|
||
| try: | ||
| client = Client(config[CONF_URL]) | ||
| client.login(config[CONF_USERNAME], config[CONF_PASSWORD]) | ||
| except LoginRequired: | ||
| _LOGGER.error("Invalid authentication") | ||
| return | ||
| except RequestException as err: | ||
| _LOGGER.error("Connection failed") | ||
| raise PlatformNotReady from err | ||
|
|
||
| name = config.get(CONF_NAME) | ||
|
|
||
| """Set up the qBittorrent platform.""" | ||
| hass.async_create_task( | ||
|
chrisx8 marked this conversation as resolved.
|
||
| hass.config_entries.flow.async_init( | ||
| DOMAIN, context={"source": SOURCE_IMPORT}, data=config | ||
| ) | ||
| ) | ||
| ir.async_create_issue( | ||
| hass, | ||
| DOMAIN, | ||
| "deprecated_yaml", | ||
| breaks_in_ha_version="2023.6.0", | ||
| is_fixable=False, | ||
| severity=ir.IssueSeverity.WARNING, | ||
| translation_key="deprecated_yaml", | ||
| ) | ||
|
|
||
|
|
||
| async def async_setup_entry( | ||
| hass: HomeAssistant, | ||
| config_entry: ConfigEntry, | ||
| async_add_entites: AddEntitiesCallback, | ||
| ) -> None: | ||
| """Set up qBittorrent sensor entries.""" | ||
| client: Client = hass.data[DOMAIN][config_entry.entry_id] | ||
| entities = [ | ||
| QBittorrentSensor(description, client, name) for description in SENSOR_TYPES | ||
| QBittorrentSensor(description, client, config_entry) | ||
| for description in SENSOR_TYPES | ||
| ] | ||
|
|
||
| add_entities(entities, True) | ||
| async_add_entites(entities, True) | ||
|
|
||
|
|
||
| def format_speed(speed): | ||
|
|
@@ -108,14 +119,15 @@ class QBittorrentSensor(SensorEntity): | |
| def __init__( | ||
| self, | ||
| description: SensorEntityDescription, | ||
| qbittorrent_client, | ||
| client_name, | ||
| qbittorrent_client: Client, | ||
| config_entry: ConfigEntry, | ||
| ) -> None: | ||
| """Initialize the qBittorrent sensor.""" | ||
| self.entity_description = description | ||
| self.client = qbittorrent_client | ||
|
|
||
| self._attr_name = f"{client_name} {description.name}" | ||
| self._attr_unique_id = f"{config_entry.entry_id}-{description.key}" | ||
| self._attr_name = f"{config_entry.title} {description.name}" | ||
|
Contributor
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. You should now look at adding |
||
| self._attr_available = False | ||
|
|
||
| def update(self) -> None: | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| { | ||
| "config": { | ||
| "step": { | ||
| "user": { | ||
| "data": { | ||
| "username": "[%key:common::config_flow::data::username%]", | ||
| "password": "[%key:common::config_flow::data::password%]", | ||
| "url": "[%key:common::config_flow::data::url%]", | ||
| "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" | ||
| } | ||
| } | ||
| }, | ||
| "error": { | ||
| "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||
| "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" | ||
| }, | ||
| "abort": { | ||
| "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||
| } | ||
| }, | ||
| "issues": { | ||
| "deprecated_yaml": { | ||
| "title": "The qBittorrent YAML configuration is being removed", | ||
| "description": "Configuring qBittorrent using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the qBittorrent YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -339,6 +339,7 @@ | |
| "pushover", | ||
| "pvoutput", | ||
| "pvpc_hourly_pricing", | ||
| "qbittorrent", | ||
| "qingping", | ||
| "qnap_qsw", | ||
| "rachio", | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Tests for the qBittorrent integration.""" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| """Fixtures for testing qBittorrent component.""" | ||
| from collections.abc import Generator | ||
| from unittest.mock import AsyncMock, patch | ||
|
|
||
| import pytest | ||
| import requests_mock | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def mock_setup_entry() -> Generator[AsyncMock, None, None]: | ||
| """Mock qbittorrent entry setup.""" | ||
| with patch( | ||
| "homeassistant.components.qbittorrent.async_setup_entry", return_value=True | ||
| ) as mock_setup_entry: | ||
| yield mock_setup_entry | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def mock_api() -> Generator[requests_mock.Mocker, None, None]: | ||
| """Mock the qbittorrent API.""" | ||
| with requests_mock.Mocker() as mocker: | ||
| mocker.get("http://localhost:8080/api/v2/app/preferences", status_code=403) | ||
| mocker.get("http://localhost:8080/api/v2/transfer/speedLimitsMode") | ||
| mocker.post("http://localhost:8080/api/v2/auth/login", text="Ok.") | ||
| yield mocker |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.