Add button platform to pyLoad integration (#120359)
This commit is contained in:
parent
adc074f60a
commit
fd0fee1900
8 changed files with 420 additions and 2 deletions
|
@ -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]
|
||||
|
||||
|
|
107
homeassistant/components/pyload/button.py
Normal file
107
homeassistant/components/pyload/button.py
Normal 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)
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
185
tests/components/pyload/snapshots/test_button.ambr
Normal file
185
tests/components/pyload/snapshots/test_button.ambr
Normal 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',
|
||||
})
|
||||
# ---
|
83
tests/components/pyload/test_button.py
Normal file
83
tests/components/pyload/test_button.py
Normal 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()
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue