From c265c91ef2111b285401426947489f6f0fff6496 Mon Sep 17 00:00:00 2001 From: Mark Grandi Date: Wed, 2 Oct 2024 07:44:56 -0700 Subject: [PATCH] Add protocol upload / download sensors to Deluge (#119203) * Add Protocol Upload/Download for Deluge * add unit test and fix typo in sensor.py * remove unneeded import * rename/unify the translation keys and entries in const.py * split out const.py items into DelugeSensorType to avoid confusion with DelugeGetSessionStatusKeys * change DelugeGetSessionStatusKeys to be a regular enum to satisfy mypy --- homeassistant/components/deluge/const.py | 38 +++++++++-- .../components/deluge/coordinator.py | 4 +- homeassistant/components/deluge/sensor.py | 67 +++++++++++++++---- homeassistant/components/deluge/strings.json | 6 ++ tests/components/deluge/__init__.py | 7 ++ tests/components/deluge/test_sensor.py | 32 +++++++++ 6 files changed, 134 insertions(+), 20 deletions(-) create mode 100644 tests/components/deluge/test_sensor.py diff --git a/homeassistant/components/deluge/const.py b/homeassistant/components/deluge/const.py index 91e08da3470f11..a76817519da9ef 100644 --- a/homeassistant/components/deluge/const.py +++ b/homeassistant/components/deluge/const.py @@ -1,17 +1,45 @@ """Constants for the Deluge integration.""" +import enum import logging from typing import Final CONF_WEB_PORT = "web_port" -CURRENT_STATUS = "current_status" -DATA_KEYS = ["upload_rate", "download_rate", "dht_upload_rate", "dht_download_rate"] DEFAULT_NAME = "Deluge" DEFAULT_RPC_PORT = 58846 DEFAULT_WEB_PORT = 8112 DOMAIN: Final = "deluge" -DOWNLOAD_SPEED = "download_speed" - LOGGER = logging.getLogger(__package__) -UPLOAD_SPEED = "upload_speed" + +class DelugeGetSessionStatusKeys(enum.Enum): + """Enum representing the keys that get passed into the Deluge RPC `core.get_session_status` xml rpc method. + + You can call `core.get_session_status` with no keys (so an empty list in deluge-client.DelugeRPCClient.call) + to get the full list of possible keys, but it seems to basically be a all of the session statistics + listed on this page: https://www.rasterbar.com/products/libtorrent/manual-ref.html#session-statistics + and a few others + + there is also a list of deprecated keys that deluge will translate for you and issue a warning in the log: + https://github.com/deluge-torrent/deluge/blob/7f3f7f69ee78610e95bea07d99f699e9310c4e08/deluge/core/core.py#L58 + + """ + + DHT_DOWNLOAD_RATE = "dht_download_rate" + DHT_UPLOAD_RATE = "dht_upload_rate" + DOWNLOAD_RATE = "download_rate" + UPLOAD_RATE = "upload_rate" + + +class DelugeSensorType(enum.StrEnum): + """Enum that distinguishes the different sensor types that the Deluge integration has. + + This is mainly used to avoid passing strings around and to distinguish between similarly + named strings in `DelugeGetSessionStatusKeys`. + """ + + CURRENT_STATUS_SENSOR = "current_status" + DOWNLOAD_SPEED_SENSOR = "download_speed" + UPLOAD_SPEED_SENSOR = "upload_speed" + PROTOCOL_TRAFFIC_UPLOAD_SPEED_SENSOR = "protocol_traffic_upload_speed" + PROTOCOL_TRAFFIC_DOWNLOAD_SPEED_SENSOR = "protocol_traffic_download_speed" diff --git a/homeassistant/components/deluge/coordinator.py b/homeassistant/components/deluge/coordinator.py index 11557561be8bf8..7f4bf9e884ec3b 100644 --- a/homeassistant/components/deluge/coordinator.py +++ b/homeassistant/components/deluge/coordinator.py @@ -13,7 +13,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DATA_KEYS, LOGGER +from .const import LOGGER, DelugeGetSessionStatusKeys if TYPE_CHECKING: from . import DelugeConfigEntry @@ -46,7 +46,7 @@ async def _async_update_data(self) -> dict[Platform, dict[str, Any]]: _data = await self.hass.async_add_executor_job( self.api.call, "core.get_session_status", - DATA_KEYS, + [iter_member.value for iter_member in list(DelugeGetSessionStatusKeys)], ) data[Platform.SENSOR] = {k.decode(): v for k, v in _data.items()} data[Platform.SWITCH] = await self.hass.async_add_executor_job( diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 05f78ddf50132b..5ebf3d01eeb88c 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -18,16 +18,20 @@ from homeassistant.helpers.typing import StateType from . import DelugeConfigEntry -from .const import CURRENT_STATUS, DATA_KEYS, DOWNLOAD_SPEED, UPLOAD_SPEED +from .const import DelugeGetSessionStatusKeys, DelugeSensorType from .coordinator import DelugeDataUpdateCoordinator from .entity import DelugeEntity def get_state(data: dict[str, float], key: str) -> str | float: """Get current download/upload state.""" - upload = data[DATA_KEYS[0]] - data[DATA_KEYS[2]] - download = data[DATA_KEYS[1]] - data[DATA_KEYS[3]] - if key == CURRENT_STATUS: + upload = data[DelugeGetSessionStatusKeys.UPLOAD_RATE.value] + download = data[DelugeGetSessionStatusKeys.DOWNLOAD_RATE.value] + protocol_upload = data[DelugeGetSessionStatusKeys.DHT_UPLOAD_RATE.value] + protocol_download = data[DelugeGetSessionStatusKeys.DHT_DOWNLOAD_RATE.value] + + # if key is CURRENT_STATUS, we just return whether we are uploading / downloading / idle + if key == DelugeSensorType.CURRENT_STATUS_SENSOR: if upload > 0 and download > 0: return "seeding_and_downloading" if upload > 0 and download == 0: @@ -35,7 +39,20 @@ def get_state(data: dict[str, float], key: str) -> str | float: if upload == 0 and download > 0: return "downloading" return STATE_IDLE - kb_spd = float(upload if key == UPLOAD_SPEED else download) / 1024 + + # if not, return the transfer rate for the given key + rate = 0.0 + if key == DelugeSensorType.DOWNLOAD_SPEED_SENSOR: + rate = download + elif key == DelugeSensorType.UPLOAD_SPEED_SENSOR: + rate = upload + elif key == DelugeSensorType.PROTOCOL_TRAFFIC_DOWNLOAD_SPEED_SENSOR: + rate = protocol_download + else: + rate = protocol_upload + + # convert to KiB/s and round + kb_spd = rate / 1024 return round(kb_spd, 2 if kb_spd < 0.1 else 1) @@ -48,27 +65,51 @@ class DelugeSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: tuple[DelugeSensorEntityDescription, ...] = ( DelugeSensorEntityDescription( - key=CURRENT_STATUS, + key=DelugeSensorType.CURRENT_STATUS_SENSOR.value, translation_key="status", - value=lambda data: get_state(data, CURRENT_STATUS), + value=lambda data: get_state( + data, DelugeSensorType.CURRENT_STATUS_SENSOR.value + ), device_class=SensorDeviceClass.ENUM, options=["seeding_and_downloading", "seeding", "downloading", "idle"], ), DelugeSensorEntityDescription( - key=DOWNLOAD_SPEED, - translation_key="download_speed", + key=DelugeSensorType.DOWNLOAD_SPEED_SENSOR.value, + translation_key=DelugeSensorType.DOWNLOAD_SPEED_SENSOR.value, + device_class=SensorDeviceClass.DATA_RATE, + native_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: get_state( + data, DelugeSensorType.DOWNLOAD_SPEED_SENSOR.value + ), + ), + DelugeSensorEntityDescription( + key=DelugeSensorType.UPLOAD_SPEED_SENSOR.value, + translation_key=DelugeSensorType.UPLOAD_SPEED_SENSOR.value, + device_class=SensorDeviceClass.DATA_RATE, + native_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: get_state(data, DelugeSensorType.UPLOAD_SPEED_SENSOR.value), + ), + DelugeSensorEntityDescription( + key=DelugeSensorType.PROTOCOL_TRAFFIC_UPLOAD_SPEED_SENSOR.value, + translation_key=DelugeSensorType.PROTOCOL_TRAFFIC_UPLOAD_SPEED_SENSOR.value, device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, - value=lambda data: get_state(data, DOWNLOAD_SPEED), + value=lambda data: get_state( + data, DelugeSensorType.PROTOCOL_TRAFFIC_UPLOAD_SPEED_SENSOR.value + ), ), DelugeSensorEntityDescription( - key=UPLOAD_SPEED, - translation_key="upload_speed", + key=DelugeSensorType.PROTOCOL_TRAFFIC_DOWNLOAD_SPEED_SENSOR.value, + translation_key=DelugeSensorType.PROTOCOL_TRAFFIC_DOWNLOAD_SPEED_SENSOR.value, device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, - value=lambda data: get_state(data, UPLOAD_SPEED), + value=lambda data: get_state( + data, DelugeSensorType.PROTOCOL_TRAFFIC_DOWNLOAD_SPEED_SENSOR.value + ), ), ) diff --git a/homeassistant/components/deluge/strings.json b/homeassistant/components/deluge/strings.json index 52706f3989456b..b4654c4a48246a 100644 --- a/homeassistant/components/deluge/strings.json +++ b/homeassistant/components/deluge/strings.json @@ -37,6 +37,12 @@ "download_speed": { "name": "Download speed" }, + "protocol_traffic_download_speed": { + "name": "Protocol traffic download speed" + }, + "protocol_traffic_upload_speed": { + "name": "Protocol traffic upload speed" + }, "upload_speed": { "name": "Upload speed" } diff --git a/tests/components/deluge/__init__.py b/tests/components/deluge/__init__.py index 4efbe04cf52b37..c9027f0c11ff58 100644 --- a/tests/components/deluge/__init__.py +++ b/tests/components/deluge/__init__.py @@ -14,3 +14,10 @@ CONF_PORT: DEFAULT_RPC_PORT, CONF_WEB_PORT: DEFAULT_WEB_PORT, } + +GET_TORRENT_STATUS_RESPONSE = { + "upload_rate": 3462.0, + "download_rate": 98.5, + "dht_upload_rate": 7818.0, + "dht_download_rate": 2658.0, +} diff --git a/tests/components/deluge/test_sensor.py b/tests/components/deluge/test_sensor.py new file mode 100644 index 00000000000000..7ff6dda0b9430c --- /dev/null +++ b/tests/components/deluge/test_sensor.py @@ -0,0 +1,32 @@ +"""Test Deluge sensor.py methods.""" + +from homeassistant.components.deluge.const import DelugeSensorType +from homeassistant.components.deluge.sensor import get_state + +from . import GET_TORRENT_STATUS_RESPONSE + + +def test_get_state() -> None: + """Tests get_state() with different keys.""" + + download_result = get_state( + GET_TORRENT_STATUS_RESPONSE, DelugeSensorType.DOWNLOAD_SPEED_SENSOR + ) + assert download_result == 0.1 # round(98.5 / 1024, 2) + + upload_result = get_state( + GET_TORRENT_STATUS_RESPONSE, DelugeSensorType.UPLOAD_SPEED_SENSOR + ) + assert upload_result == 3.4 # round(3462.0 / 1024, 1) + + protocol_upload_result = get_state( + GET_TORRENT_STATUS_RESPONSE, + DelugeSensorType.PROTOCOL_TRAFFIC_UPLOAD_SPEED_SENSOR, + ) + assert protocol_upload_result == 7.6 # round(7818.0 / 1024, 1) + + protocol_download_result = get_state( + GET_TORRENT_STATUS_RESPONSE, + DelugeSensorType.PROTOCOL_TRAFFIC_DOWNLOAD_SPEED_SENSOR, + ) + assert protocol_download_result == 2.6 # round(2658.0/1024, 1)