Skip to content

Commit

Permalink
Add Overseerr integration (#133981)
Browse files Browse the repository at this point in the history
* Add Overseerr integration

* Add Overseerr integration

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix
  • Loading branch information
joostlek authored Dec 28, 2024
1 parent 565fa4e commit 268c21a
Show file tree
Hide file tree
Showing 25 changed files with 1,130 additions and 0 deletions.
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ homeassistant.components.openuv.*
homeassistant.components.oralb.*
homeassistant.components.otbr.*
homeassistant.components.overkiz.*
homeassistant.components.overseerr.*
homeassistant.components.p1_monitor.*
homeassistant.components.panel_custom.*
homeassistant.components.peblar.*
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,8 @@ build.json @home-assistant/supervisor
/tests/components/ourgroceries/ @OnFreund
/homeassistant/components/overkiz/ @imicknl
/tests/components/overkiz/ @imicknl
/homeassistant/components/overseerr/ @joostlek
/tests/components/overseerr/ @joostlek
/homeassistant/components/ovo_energy/ @timmo001
/tests/components/ovo_energy/ @timmo001
/homeassistant/components/p1_monitor/ @klaasnicolaas
Expand Down
29 changes: 29 additions & 0 deletions homeassistant/components/overseerr/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""The Overseerr integration."""

from __future__ import annotations

from homeassistant.const import Platform
from homeassistant.core import HomeAssistant

from .coordinator import OverseerrConfigEntry, OverseerrCoordinator

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


async def async_setup_entry(hass: HomeAssistant, entry: OverseerrConfigEntry) -> bool:
"""Set up Overseerr from a config entry."""

coordinator = OverseerrCoordinator(hass, entry)

await coordinator.async_config_entry_first_refresh()

entry.runtime_data = coordinator

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True


async def async_unload_entry(hass: HomeAssistant, entry: OverseerrConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
60 changes: 60 additions & 0 deletions homeassistant/components/overseerr/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Config flow for Overseerr."""

from typing import Any

from python_overseerr import OverseerrClient
from python_overseerr.exceptions import OverseerrError
import voluptuous as vol
from yarl import URL

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_SSL, CONF_URL
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import DOMAIN


class OverseerrConfigFlow(ConfigFlow, domain=DOMAIN):
"""Overseerr config flow."""

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
errors: dict[str, str] = {}
if user_input:
url = URL(user_input[CONF_URL])
if (host := url.host) is None:
errors[CONF_URL] = "invalid_host"
else:
self._async_abort_entries_match({CONF_HOST: host})
port = url.port
assert port
client = OverseerrClient(
host,
port,
user_input[CONF_API_KEY],
ssl=url.scheme == "https",
session=async_get_clientsession(self.hass),
)
try:
await client.get_request_count()
except OverseerrError:
errors["base"] = "cannot_connect"
else:
return self.async_create_entry(
title="Overseerr",
data={
CONF_HOST: host,
CONF_PORT: port,
CONF_SSL: url.scheme == "https",
CONF_API_KEY: user_input[CONF_API_KEY],
},
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{vol.Required(CONF_URL): str, vol.Required(CONF_API_KEY): str}
),
errors=errors,
)
8 changes: 8 additions & 0 deletions homeassistant/components/overseerr/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Constants for the overseerr integration."""

import logging

DOMAIN = "overseerr"
LOGGER = logging.getLogger(__package__)

REQUESTS = "requests"
50 changes: 50 additions & 0 deletions homeassistant/components/overseerr/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Define an object to coordinate fetching Overseerr data."""

from datetime import timedelta

from python_overseerr import OverseerrClient, RequestCount
from python_overseerr.exceptions import OverseerrConnectionError

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_SSL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN, LOGGER

type OverseerrConfigEntry = ConfigEntry[OverseerrCoordinator]


class OverseerrCoordinator(DataUpdateCoordinator[RequestCount]):
"""Class to manage fetching Overseerr data."""

config_entry: OverseerrConfigEntry

def __init__(self, hass: HomeAssistant, entry: OverseerrConfigEntry) -> None:
"""Initialize."""
super().__init__(
hass,
LOGGER,
name=DOMAIN,
config_entry=entry,
update_interval=timedelta(minutes=5),
)
self.client = OverseerrClient(
entry.data[CONF_HOST],
entry.data[CONF_PORT],
entry.data[CONF_API_KEY],
ssl=entry.data[CONF_SSL],
session=async_get_clientsession(hass),
)

async def _async_update_data(self) -> RequestCount:
"""Fetch data from API endpoint."""
try:
return await self.client.get_request_count()
except OverseerrConnectionError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="connection_error",
translation_placeholders={"error": str(err)},
) from err
22 changes: 22 additions & 0 deletions homeassistant/components/overseerr/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Base entity for Overseerr."""

from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN
from .coordinator import OverseerrCoordinator


class OverseerrEntity(CoordinatorEntity[OverseerrCoordinator]):
"""Defines a base Overseerr entity."""

_attr_has_entity_name = True

def __init__(self, coordinator: OverseerrCoordinator, key: str) -> None:
"""Initialize Overseerr entity."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
entry_type=DeviceEntryType.SERVICE,
)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}-{key}"
27 changes: 27 additions & 0 deletions homeassistant/components/overseerr/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"entity": {
"sensor": {
"total_requests": {
"default": "mdi:forum"
},
"movie_requests": {
"default": "mdi:movie-open"
},
"tv_requests": {
"default": "mdi:television-box"
},
"pending_requests": {
"default": "mdi:clock"
},
"declined_requests": {
"default": "mdi:movie-open-off"
},
"processing_requests": {
"default": "mdi:sync"
},
"available_requests": {
"default": "mdi:message-bulleted"
}
}
}
}
11 changes: 11 additions & 0 deletions homeassistant/components/overseerr/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"domain": "overseerr",
"name": "Overseerr",
"codeowners": ["@joostlek"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/overseerr",
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["python-overseerr==0.2.0"]
}
92 changes: 92 additions & 0 deletions homeassistant/components/overseerr/quality_scale.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
This integration does not provide additional actions.
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-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
Entities of this integration does not explicitly subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done

# Silver
action-exceptions:
status: exempt
comment: |
This integration does not provide additional actions or actionable entities.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: No options to configure
docs-installation-parameters: done
entity-unavailable:
status: done
comment: Handled by the coordinator
integration-owner: done
log-when-unavailable:
status: done
comment: Handled by the coordinator
parallel-updates: done
reauthentication-flow: todo
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: |
This integration does not support discovery.
discovery:
status: exempt
comment: |
This integration does not support discovery.
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: |
This integration has a fixed single device.
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: done
exception-translations: done
icon-translations: todo
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: |
This integration doesn't have any cases where raising an issue is needed.
stale-devices:
status: exempt
comment: |
This integration has a fixed single device.
# Platinum
async-dependency: done
inject-websession: done
strict-typing: done
Loading

0 comments on commit 268c21a

Please sign in to comment.