Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Overseerr service to get requests #134229

Merged
merged 8 commits into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions homeassistant/components/overseerr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,24 @@
)
from homeassistant.const import CONF_WEBHOOK_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.http import HomeAssistantView
from homeassistant.helpers.typing import ConfigType

from .const import DOMAIN, JSON_PAYLOAD, LOGGER, REGISTERED_NOTIFICATIONS
from .coordinator import OverseerrConfigEntry, OverseerrCoordinator
from .services import setup_services

PLATFORMS: list[Platform] = [Platform.SENSOR]

CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Overseerr component."""
setup_services(hass)
return True


async def async_setup_entry(hass: HomeAssistant, entry: OverseerrConfigEntry) -> bool:
"""Set up Overseerr from a config entry."""
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/overseerr/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@

REQUESTS = "requests"

ATTR_CONFIG_ENTRY_ID = "config_entry_id"
ATTR_STATUS = "status"
ATTR_SORT_ORDER = "sort_order"
ATTR_REQUESTED_BY = "requested_by"

REGISTERED_NOTIFICATIONS = (
NotificationType.REQUEST_PENDING_APPROVAL
| NotificationType.REQUEST_APPROVED
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/overseerr/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@
"default": "mdi:message-bulleted"
}
}
},
"services": {
"get_requests": {
"service": "mdi:multimedia"
}
}
}
15 changes: 3 additions & 12 deletions homeassistant/components/overseerr/quality_scale.yaml
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
This integration does not provide additional actions.
action-setup: done
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
This integration does not provide additional actions.
docs-actions: done
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
Expand All @@ -29,10 +23,7 @@ rules:
unique-config-entry: done

# Silver
action-exceptions:
status: exempt
comment: |
This integration does not provide additional actions or actionable entities.
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
Expand Down
115 changes: 115 additions & 0 deletions homeassistant/components/overseerr/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""Define services for the Overseerr integration."""

from dataclasses import asdict
from typing import Any, cast

from python_overseerr import OverseerrClient, OverseerrConnectionError
import voluptuous as vol

from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.util.json import JsonValueType

from .const import (
ATTR_CONFIG_ENTRY_ID,
ATTR_REQUESTED_BY,
ATTR_SORT_ORDER,
ATTR_STATUS,
DOMAIN,
LOGGER,
)
from .coordinator import OverseerrConfigEntry

SERVICE_GET_REQUESTS = "get_requests"
SERVICE_GET_REQUESTS_SCHEMA = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY_ID): str,
vol.Optional(ATTR_STATUS): vol.In(
["approved", "pending", "available", "processing", "unavailable", "failed"]
),
vol.Optional(ATTR_SORT_ORDER): vol.In(["added", "modified"]),
vol.Optional(ATTR_REQUESTED_BY): int,
}
)


def async_get_entry(hass: HomeAssistant, config_entry_id: str) -> OverseerrConfigEntry:
"""Get the Overseerr config entry."""
if not (entry := hass.config_entries.async_get_entry(config_entry_id)):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="integration_not_found",
translation_placeholders={"target": DOMAIN},
)
if entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="not_loaded",
translation_placeholders={"target": entry.title},
)
return cast(OverseerrConfigEntry, entry)


async def get_media(
client: OverseerrClient, media_type: str, identifier: int
) -> dict[str, Any]:
"""Get media details."""
media = {}
try:
if media_type == "movie":
media = asdict(await client.get_movie_details(identifier))
if media_type == "tv":
media = asdict(await client.get_tv_details(identifier))
except OverseerrConnectionError:
LOGGER.error("Could not find data for %s %s", media_type, identifier)
return {}
media["media_info"].pop("requests")
return media


def setup_services(hass: HomeAssistant) -> None:
"""Set up the services for the Overseerr integration."""

async def async_get_requests(call: ServiceCall) -> ServiceResponse:
"""Get requests made to Overseerr."""
entry = async_get_entry(hass, call.data[ATTR_CONFIG_ENTRY_ID])
client = entry.runtime_data.client
kwargs: dict[str, Any] = {}
if status := call.data.get(ATTR_STATUS):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add tests with more of the args?

kwargs["status"] = status
if sort_order := call.data.get(ATTR_SORT_ORDER):
kwargs["sort"] = sort_order
if requested_by := call.data.get(ATTR_REQUESTED_BY):
kwargs["requested_by"] = requested_by
try:
requests = await client.get_requests(**kwargs)
except OverseerrConnectionError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="connection_error",
translation_placeholders={"error": str(err)},
) from err
result: list[dict[str, Any]] = []
for request in requests:
req = asdict(request)
assert request.media.tmdb_id
req["media"] = await get_media(
client, request.media.media_type, request.media.tmdb_id
)
result.append(req)

return {"requests": cast(list[JsonValueType], result)}

hass.services.async_register(
DOMAIN,
SERVICE_GET_REQUESTS,
async_get_requests,
schema=SERVICE_GET_REQUESTS_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
30 changes: 30 additions & 0 deletions homeassistant/components/overseerr/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
get_requests:
fields:
config_entry_id:
required: true
selector:
config_entry:
integration: overseerr
status:
selector:
select:
options:
- approved
- pending
- available
- processing
- unavailable
- failed
translation_key: request_status
sort_order:
selector:
select:
options:
- added
- modified
translation_key: request_sort_order
requested_by:
selector:
number:
min: 0
mode: box
48 changes: 48 additions & 0 deletions homeassistant/components/overseerr/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,54 @@
"exceptions": {
"connection_error": {
"message": "Error connecting to the Overseerr instance: {error}"
},
"not_loaded": {
"message": "{target} is not loaded."
},
"integration_not_found": {
"message": "Integration \"{target}\" not found in registry."
}
},
"services": {
"get_requests": {
"name": "Get requests",
"description": "Get media requests from Overseerr.",
"fields": {
"config_entry_id": {
"name": "Overseerr instance",
"description": "The Overseerr instance to get requests from."
},
"status": {
"name": "Request status",
"description": "Filter the requests by status."
},
"sort_order": {
"name": "Sort order",
"description": "Sort the requests by added or modified date."
},
"requested_by": {
"name": "Requested by",
"description": "Filter the requests by the user id that requested them."
}
}
}
},
"selector": {
"request_status": {
"options": {
"approved": "Approved",
"pending": "Pending",
"available": "Available",
"processing": "Processing",
"unavailable": "Unavailable",
"failed": "Failed"
}
},
"request_sort_order": {
"options": {
"added": "Added",
"modified": "Modified"
}
}
}
}
13 changes: 11 additions & 2 deletions tests/components/overseerr/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from unittest.mock import AsyncMock, patch

import pytest
from python_overseerr import RequestCount
from python_overseerr.models import WebhookNotificationConfig
from python_overseerr import MovieDetails, RequestCount, RequestResponse
from python_overseerr.models import TVDetails, WebhookNotificationConfig

from homeassistant.components.overseerr.const import DOMAIN
from homeassistant.const import (
Expand Down Expand Up @@ -54,6 +54,15 @@ def mock_overseerr_client() -> Generator[AsyncMock]:
)
)
client.test_webhook_notification_config.return_value = True
client.get_requests.return_value = RequestResponse.from_json(
load_fixture("requests.json", DOMAIN)
).results
client.get_movie_details.return_value = MovieDetails.from_json(
load_fixture("movie.json", DOMAIN)
)
client.get_tv_details.return_value = TVDetails.from_json(
load_fixture("tv.json", DOMAIN)
)
yield client


Expand Down
Loading