Add qBittorrent torrent sensors (#105781)

* Upgrade QBittorrent integration to show torrents

This brings the QBittorrent integration to be more in line with the Transmission integration. It updates how the integration is written, along with adding sensors for Active Torrents, Inactive Torrents, Paused Torrents, Total Torrents, Seeding Torrents, Started Torrents.

* Remove unused stuff

* Fix codeowners

* Correct name in comments

* Update __init__.py

* Make get torrents a service with a response

* Update sensor.py

* Update sensor.py

* Update sensor.py

* Add new sensors

* remove service

* more removes

* more

* Address comments

* cleanup

* Update coordinator.py

* Fix most lint issues

* Update sensor.py

* Update sensor.py

* Update manifest.json

* Update sensor class

* Update sensor.py

* Fix lint issue with sensor class

* Adding codeowners

* Update homeassistant/components/qbittorrent/__init__.py

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Joe Neuman 2023-12-27 00:48:52 -08:00 committed by GitHub
parent c7eab49c70
commit d33ad57dd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 151 additions and 64 deletions

View File

@ -1020,8 +1020,8 @@ build.json @home-assistant/supervisor
/tests/components/pvoutput/ @frenck
/homeassistant/components/pvpc_hourly_pricing/ @azogue
/tests/components/pvpc_hourly_pricing/ @azogue
/homeassistant/components/qbittorrent/ @geoffreylagaisse
/tests/components/qbittorrent/ @geoffreylagaisse
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
/tests/components/qbittorrent/ @geoffreylagaisse @finder39
/homeassistant/components/qingping/ @bdraco @skgsergio
/tests/components/qingping/ @bdraco @skgsergio
/homeassistant/components/qld_bushfire/ @exxamalte

View File

@ -19,21 +19,21 @@ from .const import DOMAIN
from .coordinator import QBittorrentDataCoordinator
from .helpers import setup_client
PLATFORMS = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up qBittorrent from a config entry."""
hass.data.setdefault(DOMAIN, {})
try:
client = 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],
config_entry.data[CONF_URL],
config_entry.data[CONF_USERNAME],
config_entry.data[CONF_PASSWORD],
config_entry.data[CONF_VERIFY_SSL],
)
except LoginRequired as err:
raise ConfigEntryNotReady("Invalid credentials") from err
@ -42,16 +42,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = QBittorrentDataCoordinator(hass, client)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, config_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 unload_ok := await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
):
del hass.data[DOMAIN][config_entry.entry_id]
if not hass.data[DOMAIN]:
del hass.data[DOMAIN]
return unload_ok

View File

@ -5,3 +5,7 @@ DOMAIN: Final = "qbittorrent"
DEFAULT_NAME = "qBittorrent"
DEFAULT_URL = "http://127.0.0.1:8080"
STATE_UP_DOWN = "up_down"
STATE_SEEDING = "seeding"
STATE_DOWNLOADING = "downloading"

View File

@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__)
class QBittorrentDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""QBittorrent update coordinator."""
"""Coordinator for updating QBittorrent data."""
def __init__(self, hass: HomeAssistant, client: Client) -> None:
"""Initialize coordinator."""

View File

@ -1,7 +1,7 @@
{
"domain": "qbittorrent",
"name": "qBittorrent",
"codeowners": ["@geoffreylagaisse"],
"codeowners": ["@geoffreylagaisse", "@finder39"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/qbittorrent",
"integration_type": "service",

View File

@ -4,22 +4,21 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Any
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_IDLE, UnitOfDataRate
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .const import DOMAIN, STATE_DOWNLOADING, STATE_SEEDING, STATE_UP_DOWN
from .coordinator import QBittorrentDataCoordinator
_LOGGER = logging.getLogger(__name__)
@ -27,62 +26,94 @@ _LOGGER = logging.getLogger(__name__)
SENSOR_TYPE_CURRENT_STATUS = "current_status"
SENSOR_TYPE_DOWNLOAD_SPEED = "download_speed"
SENSOR_TYPE_UPLOAD_SPEED = "upload_speed"
SENSOR_TYPE_ALL_TORRENTS = "all_torrents"
SENSOR_TYPE_PAUSED_TORRENTS = "paused_torrents"
SENSOR_TYPE_ACTIVE_TORRENTS = "active_torrents"
SENSOR_TYPE_INACTIVE_TORRENTS = "inactive_torrents"
@dataclass(frozen=True)
class QBittorrentMixin:
"""Mixin for required keys."""
value_fn: Callable[[dict[str, Any]], StateType]
@dataclass(frozen=True)
class QBittorrentSensorEntityDescription(SensorEntityDescription, QBittorrentMixin):
"""Describes QBittorrent sensor entity."""
def _get_qbittorrent_state(data: dict[str, Any]) -> str:
download = data["server_state"]["dl_info_speed"]
upload = data["server_state"]["up_info_speed"]
def get_state(coordinator: QBittorrentDataCoordinator) -> str:
"""Get current download/upload state."""
upload = coordinator.data["server_state"]["up_info_speed"]
download = coordinator.data["server_state"]["dl_info_speed"]
if upload > 0 and download > 0:
return "up_down"
return STATE_UP_DOWN
if upload > 0 and download == 0:
return "seeding"
return STATE_SEEDING
if upload == 0 and download > 0:
return "downloading"
return STATE_DOWNLOADING
return STATE_IDLE
def format_speed(speed):
"""Return a bytes/s measurement as a human readable string."""
kb_spd = float(speed) / 1024
return round(kb_spd, 2 if kb_spd < 0.1 else 1)
@dataclass(frozen=True, kw_only=True)
class QBittorrentSensorEntityDescription(SensorEntityDescription):
"""Entity description class for qBittorent sensors."""
value_fn: Callable[[QBittorrentDataCoordinator], StateType]
SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = (
QBittorrentSensorEntityDescription(
key=SENSOR_TYPE_CURRENT_STATUS,
name="Status",
value_fn=_get_qbittorrent_state,
translation_key="current_status",
device_class=SensorDeviceClass.ENUM,
options=[STATE_IDLE, STATE_UP_DOWN, STATE_SEEDING, STATE_DOWNLOADING],
value_fn=get_state,
),
QBittorrentSensorEntityDescription(
key=SENSOR_TYPE_DOWNLOAD_SPEED,
name="Down Speed",
translation_key="download_speed",
icon="mdi:cloud-download",
device_class=SensorDeviceClass.DATA_RATE,
native_unit_of_measurement=UnitOfDataRate.KIBIBYTES_PER_SECOND,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: format_speed(data["server_state"]["dl_info_speed"]),
native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
suggested_display_precision=2,
suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND,
value_fn=lambda coordinator: float(
coordinator.data["server_state"]["dl_info_speed"]
),
),
QBittorrentSensorEntityDescription(
key=SENSOR_TYPE_UPLOAD_SPEED,
name="Up Speed",
translation_key="upload_speed",
icon="mdi:cloud-upload",
device_class=SensorDeviceClass.DATA_RATE,
native_unit_of_measurement=UnitOfDataRate.KIBIBYTES_PER_SECOND,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: format_speed(data["server_state"]["up_info_speed"]),
native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
suggested_display_precision=2,
suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND,
value_fn=lambda coordinator: float(
coordinator.data["server_state"]["up_info_speed"]
),
),
QBittorrentSensorEntityDescription(
key=SENSOR_TYPE_ALL_TORRENTS,
translation_key="all_torrents",
native_unit_of_measurement="torrents",
value_fn=lambda coordinator: count_torrents_in_states(coordinator, []),
),
QBittorrentSensorEntityDescription(
key=SENSOR_TYPE_ACTIVE_TORRENTS,
translation_key="active_torrents",
native_unit_of_measurement="torrents",
value_fn=lambda coordinator: count_torrents_in_states(
coordinator, ["downloading", "uploading"]
),
),
QBittorrentSensorEntityDescription(
key=SENSOR_TYPE_INACTIVE_TORRENTS,
translation_key="inactive_torrents",
native_unit_of_measurement="torrents",
value_fn=lambda coordinator: count_torrents_in_states(
coordinator, ["stalledDL", "stalledUP"]
),
),
QBittorrentSensorEntityDescription(
key=SENSOR_TYPE_PAUSED_TORRENTS,
translation_key="paused_torrents",
native_unit_of_measurement="torrents",
value_fn=lambda coordinator: count_torrents_in_states(
coordinator, ["pausedDL", "pausedUP"]
),
),
)
@ -90,36 +121,54 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entites: AddEntitiesCallback,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up qBittorrent sensor entries."""
coordinator: QBittorrentDataCoordinator = hass.data[DOMAIN][config_entry.entry_id]
entities = [
QBittorrentSensor(description, coordinator, config_entry)
async_add_entities(
QBittorrentSensor(coordinator, config_entry, description)
for description in SENSOR_TYPES
]
async_add_entites(entities)
)
class QBittorrentSensor(CoordinatorEntity[QBittorrentDataCoordinator], SensorEntity):
"""Representation of a qBittorrent sensor."""
_attr_has_entity_name = True
entity_description: QBittorrentSensorEntityDescription
def __init__(
self,
description: QBittorrentSensorEntityDescription,
coordinator: QBittorrentDataCoordinator,
config_entry: ConfigEntry,
entity_description: QBittorrentSensorEntityDescription,
) -> None:
"""Initialize the qBittorrent sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{config_entry.entry_id}-{description.key}"
self._attr_name = f"{config_entry.title} {description.name}"
self._attr_available = False
self.entity_description = entity_description
self._attr_unique_id = f"{config_entry.entry_id}-{entity_description.key}"
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, config_entry.entry_id)},
manufacturer="QBittorrent",
)
@property
def native_value(self) -> StateType:
"""Return value of sensor."""
return self.entity_description.value_fn(self.coordinator.data)
"""Return the value of the sensor."""
return self.entity_description.value_fn(self.coordinator)
def count_torrents_in_states(
coordinator: QBittorrentDataCoordinator, states: list[str]
) -> int:
"""Count the number of torrents in specified states."""
return len(
[
torrent
for torrent in coordinator.data["torrents"].values()
if torrent["state"] in states
]
)

View File

@ -17,5 +17,36 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"entity": {
"sensor": {
"download_speed": {
"name": "Download speed"
},
"upload_speed": {
"name": "Upload speed"
},
"transmission_status": {
"name": "Status",
"state": {
"idle": "[%key:common::state::idle%]",
"up_down": "Up/Down",
"seeding": "Seeding",
"downloading": "Downloading"
}
},
"active_torrents": {
"name": "Active torrents"
},
"inactive_torrents": {
"name": "Inactive torrents"
},
"paused_torrents": {
"name": "Paused torrents"
},
"all_torrents": {
"name": "All torrents"
}
}
}
}