Add button platform to pyLoad integration (#120359)

This commit is contained in:
Mr. Bubbles 2024-06-25 08:09:54 +02:00 committed by GitHub
parent adc074f60a
commit fd0fee1900
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 420 additions and 2 deletions

View file

@ -22,7 +22,7 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .coordinator import PyLoadCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR]
PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SENSOR]
type PyLoadConfigEntry = ConfigEntry[PyLoadCoordinator]

View file

@ -0,0 +1,107 @@
"""Support for monitoring pyLoad."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from enum import StrEnum
from typing import Any
from pyloadapi.api import PyLoadAPI
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import PyLoadConfigEntry
from .const import DOMAIN, MANUFACTURER, SERVICE_NAME
from .coordinator import PyLoadCoordinator
@dataclass(kw_only=True, frozen=True)
class PyLoadButtonEntityDescription(ButtonEntityDescription):
"""Describes pyLoad button entity."""
press_fn: Callable[[PyLoadAPI], Awaitable[Any]]
class PyLoadButtonEntity(StrEnum):
"""PyLoad button Entities."""
ABORT_DOWNLOADS = "abort_downloads"
RESTART_FAILED = "restart_failed"
DELETE_FINISHED = "delete_finished"
RESTART = "restart"
SENSOR_DESCRIPTIONS: tuple[PyLoadButtonEntityDescription, ...] = (
PyLoadButtonEntityDescription(
key=PyLoadButtonEntity.ABORT_DOWNLOADS,
translation_key=PyLoadButtonEntity.ABORT_DOWNLOADS,
press_fn=lambda api: api.stop_all_downloads(),
),
PyLoadButtonEntityDescription(
key=PyLoadButtonEntity.RESTART_FAILED,
translation_key=PyLoadButtonEntity.RESTART_FAILED,
press_fn=lambda api: api.restart_failed(),
),
PyLoadButtonEntityDescription(
key=PyLoadButtonEntity.DELETE_FINISHED,
translation_key=PyLoadButtonEntity.DELETE_FINISHED,
press_fn=lambda api: api.delete_finished(),
),
PyLoadButtonEntityDescription(
key=PyLoadButtonEntity.RESTART,
translation_key=PyLoadButtonEntity.RESTART,
press_fn=lambda api: api.restart(),
entity_registry_enabled_default=False,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: PyLoadConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up buttons from a config entry."""
coordinator = entry.runtime_data
async_add_entities(
PyLoadBinarySensor(coordinator, description)
for description in SENSOR_DESCRIPTIONS
)
class PyLoadBinarySensor(CoordinatorEntity[PyLoadCoordinator], ButtonEntity):
"""Representation of a pyLoad button."""
_attr_has_entity_name = True
entity_description: PyLoadButtonEntityDescription
def __init__(
self,
coordinator: PyLoadCoordinator,
entity_description: PyLoadButtonEntityDescription,
) -> None:
"""Initialize the button."""
super().__init__(coordinator)
self._attr_unique_id = (
f"{coordinator.config_entry.entry_id}_{entity_description.key}"
)
self.entity_description = entity_description
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
manufacturer=MANUFACTURER,
model=SERVICE_NAME,
configuration_url=coordinator.pyload.api_url,
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
translation_key=DOMAIN,
)
async def async_press(self) -> None:
"""Handle the button press."""
await self.entity_description.press_fn(self.coordinator.pyload)

View file

@ -7,3 +7,6 @@ DEFAULT_NAME = "pyLoad"
DEFAULT_PORT = 8000
ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=pyload"}
MANUFACTURER = "pyLoad Team"
SERVICE_NAME = "pyLoad"

View file

@ -1,5 +1,19 @@
{
"entity": {
"button": {
"abort_downloads": {
"default": "mdi:stop"
},
"restart_failed": {
"default": "mdi:cached"
},
"delete_finished": {
"default": "mdi:trash-can"
},
"restart": {
"default": "mdi:restart"
}
},
"sensor": {
"speed": {
"default": "mdi:speedometer"

View file

@ -28,6 +28,20 @@
}
},
"entity": {
"button": {
"abort_downloads": {
"name": "Abort all running downloads"
},
"restart_failed": {
"name": "Restart all failed files"
},
"delete_finished": {
"name": "Delete finished files/packages"
},
"restart": {
"name": "Restart pyload core"
}
},
"sensor": {
"speed": {
"name": "Speed"

View file

@ -0,0 +1,185 @@
# serializer version: 1
# name: test_state[button.pyload_abort_all_running_downloads-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.pyload_abort_all_running_downloads',
'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': 'Abort all running downloads',
'platform': 'pyload',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <PyLoadButtonEntity.ABORT_DOWNLOADS: 'abort_downloads'>,
'unique_id': 'XXXXXXXXXXXXXX_abort_downloads',
'unit_of_measurement': None,
})
# ---
# name: test_state[button.pyload_abort_all_running_downloads-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'pyload Abort all running downloads',
}),
'context': <ANY>,
'entity_id': 'button.pyload_abort_all_running_downloads',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_state[button.pyload_delete_finished_files_packages-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.pyload_delete_finished_files_packages',
'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': 'Delete finished files/packages',
'platform': 'pyload',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <PyLoadButtonEntity.DELETE_FINISHED: 'delete_finished'>,
'unique_id': 'XXXXXXXXXXXXXX_delete_finished',
'unit_of_measurement': None,
})
# ---
# name: test_state[button.pyload_delete_finished_files_packages-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'pyload Delete finished files/packages',
}),
'context': <ANY>,
'entity_id': 'button.pyload_delete_finished_files_packages',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_state[button.pyload_restart_all_failed_files-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.pyload_restart_all_failed_files',
'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': 'Restart all failed files',
'platform': 'pyload',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <PyLoadButtonEntity.RESTART_FAILED: 'restart_failed'>,
'unique_id': 'XXXXXXXXXXXXXX_restart_failed',
'unit_of_measurement': None,
})
# ---
# name: test_state[button.pyload_restart_all_failed_files-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'pyload Restart all failed files',
}),
'context': <ANY>,
'entity_id': 'button.pyload_restart_all_failed_files',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_state[button.pyload_restart_pyload_core-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.pyload_restart_pyload_core',
'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': 'Restart pyload core',
'platform': 'pyload',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <PyLoadButtonEntity.RESTART: 'restart'>,
'unique_id': 'XXXXXXXXXXXXXX_restart',
'unit_of_measurement': None,
})
# ---
# name: test_state[button.pyload_restart_pyload_core-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'pyload Restart pyload core',
}),
'context': <ANY>,
'entity_id': 'button.pyload_restart_pyload_core',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---

View file

@ -0,0 +1,83 @@
"""The tests for the button component."""
from collections.abc import AsyncGenerator
from unittest.mock import AsyncMock, call, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.components.pyload.button import PyLoadButtonEntity
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, snapshot_platform
API_CALL = {
PyLoadButtonEntity.ABORT_DOWNLOADS: call.stop_all_downloads,
PyLoadButtonEntity.RESTART_FAILED: call.restart_failed,
PyLoadButtonEntity.DELETE_FINISHED: call.delete_finished,
PyLoadButtonEntity.RESTART: call.restart,
}
@pytest.fixture(autouse=True)
async def button_only() -> AsyncGenerator[None, None]:
"""Enable only the button platform."""
with patch(
"homeassistant.components.pyload.PLATFORMS",
[Platform.BUTTON],
):
yield
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_state(
hass: HomeAssistant,
config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_pyloadapi: AsyncMock,
) -> None:
"""Test button state."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_button_press(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_pyloadapi: AsyncMock,
entity_registry: er.EntityRegistry,
) -> None:
"""Test switch turn on method."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
entity_entries = er.async_entries_for_config_entry(
entity_registry, config_entry.entry_id
)
for entity_entry in entity_entries:
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: entity_entry.entity_id},
blocking=True,
)
await hass.async_block_till_done()
assert API_CALL[entity_entry.translation_key] in mock_pyloadapi.method_calls
mock_pyloadapi.reset_mock()

View file

@ -1,6 +1,7 @@
"""Tests for the pyLoad Sensors."""
from unittest.mock import AsyncMock
from collections.abc import AsyncGenerator
from unittest.mock import AsyncMock, patch
from freezegun.api import FrozenDateTimeFactory
from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError
@ -11,6 +12,7 @@ from homeassistant.components.pyload.const import DOMAIN
from homeassistant.components.pyload.coordinator import SCAN_INTERVAL
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.helpers.typing import ConfigType
@ -19,6 +21,16 @@ from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@pytest.fixture(autouse=True)
async def sensor_only() -> AsyncGenerator[None, None]:
"""Enable only the sensor platform."""
with patch(
"homeassistant.components.pyload.PLATFORMS",
[Platform.SENSOR],
):
yield
async def test_setup(
hass: HomeAssistant,
config_entry: MockConfigEntry,