Add number platform to sabnzbd and deprecate custom action (#131029)

* Add number platform to sabnzbd

* Copy & waste error

* Move to icon translations

* Update snapshot
This commit is contained in:
Jan-Philipp Benecke 2024-11-20 19:59:10 +01:00 committed by GitHub
parent 309dd5ed1b
commit 06db5a55f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 286 additions and 1 deletions

View File

@ -27,7 +27,7 @@ from .const import (
from .coordinator import SabnzbdUpdateCoordinator
from .sab import get_client
PLATFORMS = [Platform.BUTTON, Platform.SENSOR]
PLATFORMS = [Platform.BUTTON, Platform.NUMBER, Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
SERVICES = (
@ -128,6 +128,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_set_queue_speed(
call: ServiceCall, coordinator: SabnzbdUpdateCoordinator
) -> None:
ir.async_create_issue(
hass,
DOMAIN,
"set_speed_action_deprecated",
is_fixable=False,
severity=ir.IssueSeverity.WARNING,
breaks_in_ha_version="2025.6",
translation_key="set_speed_action_deprecated",
)
speed = call.data.get(ATTR_SPEED)
await coordinator.sab_api.set_speed_limit(speed)

View File

@ -6,6 +6,9 @@
},
"resume": {
"default": "mdi:play"
},
"speedlimit": {
"default": "mdi:speedometer"
}
}
},

View File

@ -0,0 +1,82 @@
"""Number entities for the SABnzbd integration."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from pysabnzbd import SabnzbdApiException
from homeassistant.components.number import (
NumberEntity,
NumberEntityDescription,
NumberMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import SabnzbdUpdateCoordinator
from .entity import SabnzbdEntity
@dataclass(frozen=True, kw_only=True)
class SabnzbdNumberEntityDescription(NumberEntityDescription):
"""Class describing a SABnzbd number entities."""
set_fn: Callable[[SabnzbdUpdateCoordinator, float], Awaitable]
NUMBER_DESCRIPTIONS: tuple[SabnzbdNumberEntityDescription, ...] = (
SabnzbdNumberEntityDescription(
key="speedlimit",
translation_key="speedlimit",
mode=NumberMode.BOX,
native_max_value=100,
native_min_value=0,
native_step=1,
native_unit_of_measurement=PERCENTAGE,
set_fn=lambda coordinator, speed: (
coordinator.sab_api.set_speed_limit(int(speed))
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the SABnzbd number entity."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities(
SabnzbdNumber(coordinator, description) for description in NUMBER_DESCRIPTIONS
)
class SabnzbdNumber(SabnzbdEntity, NumberEntity):
"""Representation of a SABnzbd number."""
entity_description: SabnzbdNumberEntityDescription
@property
def native_value(self) -> float:
"""Return latest value for number."""
return self.coordinator.data[self.entity_description.key]
async def async_set_native_value(self, value: float) -> None:
"""Set the new number value."""
try:
await self.entity_description.set_fn(self.coordinator, value)
except SabnzbdApiException as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="service_call_exception",
) from e
else:
await self.coordinator.async_request_refresh()

View File

@ -26,6 +26,11 @@
"name": "[%key:component::sabnzbd::services::resume::name%]"
}
},
"number": {
"speedlimit": {
"name": "Speedlimit"
}
},
"sensor": {
"status": {
"name": "Status"
@ -106,6 +111,10 @@
"resume_action_deprecated": {
"title": "SABnzbd resume action deprecated",
"description": "The 'Resume' action is deprecated and will be removed in a future version. Please use the 'Resume' button instead. To remove this issue, please adjust automations accordingly and restart Home Assistant."
},
"set_speed_action_deprecated": {
"title": "SABnzbd set_speed action deprecated",
"description": "The 'Set speed' action is deprecated and will be removed in a future version. Please use the 'Speedlimit' number entity instead. To remove this issue, please adjust automations accordingly and restart Home Assistant."
}
},
"exceptions": {

View File

@ -0,0 +1,57 @@
# serializer version: 1
# name: test_number_setup[number.sabnzbd_speedlimit-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.sabnzbd_speedlimit',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Speedlimit',
'platform': 'sabnzbd',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'speedlimit',
'unique_id': '01JD2YVVPBC62D620DGYNG2R8H_speedlimit',
'unit_of_measurement': '%',
})
# ---
# name: test_number_setup[number.sabnzbd_speedlimit-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Sabnzbd Speedlimit',
'max': 100,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.sabnzbd_speedlimit',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '85',
})
# ---

View File

@ -7,6 +7,7 @@ from homeassistant.components.sabnzbd.const import (
DOMAIN,
SERVICE_PAUSE,
SERVICE_RESUME,
SERVICE_SET_SPEED,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
@ -17,6 +18,7 @@ from homeassistant.helpers import issue_registry as ir
[
(SERVICE_RESUME, "resume_action_deprecated"),
(SERVICE_PAUSE, "pause_action_deprecated"),
(SERVICE_SET_SPEED, "set_speed_action_deprecated"),
],
)
@pytest.mark.usefixtures("setup_integration")

View File

@ -0,0 +1,123 @@
"""Number tests for the SABnzbd component."""
from datetime import timedelta
from unittest.mock import AsyncMock, patch
from freezegun.api import FrozenDateTimeFactory
from pysabnzbd import SabnzbdApiException
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.number import (
ATTR_VALUE,
DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@patch("homeassistant.components.sabnzbd.PLATFORMS", [Platform.NUMBER])
async def test_number_setup(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test number setup."""
await hass.config_entries.async_setup(config_entry.entry_id)
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
@pytest.mark.parametrize(
("number", "input_number", "called_function", "expected_state"),
[
("speedlimit", 50.0, "set_speed_limit", 50),
],
)
@pytest.mark.usefixtures("setup_integration")
async def test_number_set(
hass: HomeAssistant,
sabnzbd: AsyncMock,
number: str,
input_number: float,
called_function: str,
expected_state: str,
) -> None:
"""Test the sabnzbd number set."""
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_VALUE: input_number,
ATTR_ENTITY_ID: f"number.sabnzbd_{number}",
},
blocking=True,
)
function = getattr(sabnzbd, called_function)
function.assert_called_with(int(input_number))
@pytest.mark.parametrize(
("number", "input_number", "called_function"),
[("speedlimit", 55.0, "set_speed_limit")],
)
@pytest.mark.usefixtures("setup_integration")
async def test_number_exception(
hass: HomeAssistant,
sabnzbd: AsyncMock,
number: str,
input_number: float,
called_function: str,
) -> None:
"""Test the number entity handles errors."""
function = getattr(sabnzbd, called_function)
function.side_effect = SabnzbdApiException("Boom")
with pytest.raises(
HomeAssistantError,
match="Unable to send command to SABnzbd due to a connection error, try again later",
):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_VALUE: input_number,
ATTR_ENTITY_ID: f"number.sabnzbd_{number}",
},
blocking=True,
)
function.assert_called_once()
@pytest.mark.parametrize(
("number", "initial_state"),
[("speedlimit", "85")],
)
@pytest.mark.usefixtures("setup_integration")
async def test_number_unavailable(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
sabnzbd: AsyncMock,
number: str,
initial_state: str,
) -> None:
"""Test the number is unavailable when coordinator can't update data."""
state = hass.states.get(f"number.sabnzbd_{number}")
assert state
assert state.state == initial_state
sabnzbd.refresh_data.side_effect = Exception("Boom")
freezer.tick(timedelta(minutes=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(f"number.sabnzbd_{number}")
assert state
assert state.state == STATE_UNAVAILABLE