diff --git a/CODEOWNERS b/CODEOWNERS index b50af486033..b0dcda5ce27 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/homeassistant/components/qbittorrent/__init__.py b/homeassistant/components/qbittorrent/__init__.py index fd9577f5c73..84315186097 100644 --- a/homeassistant/components/qbittorrent/__init__.py +++ b/homeassistant/components/qbittorrent/__init__.py @@ -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 diff --git a/homeassistant/components/qbittorrent/const.py b/homeassistant/components/qbittorrent/const.py index 0a79c67f400..96c60e9b380 100644 --- a/homeassistant/components/qbittorrent/const.py +++ b/homeassistant/components/qbittorrent/const.py @@ -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" diff --git a/homeassistant/components/qbittorrent/coordinator.py b/homeassistant/components/qbittorrent/coordinator.py index 8363a764d0a..11467ce62f4 100644 --- a/homeassistant/components/qbittorrent/coordinator.py +++ b/homeassistant/components/qbittorrent/coordinator.py @@ -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.""" diff --git a/homeassistant/components/qbittorrent/manifest.json b/homeassistant/components/qbittorrent/manifest.json index e2c1526e4f8..fb51f177081 100644 --- a/homeassistant/components/qbittorrent/manifest.json +++ b/homeassistant/components/qbittorrent/manifest.json @@ -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", diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 0e6bc071125..a51ff58405c 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -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 + ] + ) diff --git a/homeassistant/components/qbittorrent/strings.json b/homeassistant/components/qbittorrent/strings.json index 66c9430911e..8b20a3354dd 100644 --- a/homeassistant/components/qbittorrent/strings.json +++ b/homeassistant/components/qbittorrent/strings.json @@ -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" + } + } } }