mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 01:37:08 +00:00
Implement cloudhooks for Overseerr (#134680)
This commit is contained in:
parent
77221f53b3
commit
f5d35bca72
@ -3,12 +3,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import cast
|
||||
|
||||
from aiohttp.hdrs import METH_POST
|
||||
from aiohttp.web_request import Request
|
||||
from aiohttp.web_response import Response
|
||||
from python_overseerr import OverseerrConnectionError
|
||||
|
||||
from homeassistant.components import cloud
|
||||
from homeassistant.components.webhook import (
|
||||
async_generate_url,
|
||||
async_register,
|
||||
@ -26,6 +28,7 @@ from .coordinator import OverseerrConfigEntry, OverseerrCoordinator
|
||||
from .services import setup_services
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR]
|
||||
CONF_CLOUDHOOK_URL = "cloudhook_url"
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
@ -64,6 +67,18 @@ async def async_unload_entry(hass: HomeAssistant, entry: OverseerrConfigEntry) -
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: OverseerrConfigEntry) -> None:
|
||||
"""Cleanup when entry is removed."""
|
||||
if cloud.async_active_subscription(hass):
|
||||
try:
|
||||
LOGGER.debug(
|
||||
"Removing Overseerr cloudhook (%s)", entry.data[CONF_WEBHOOK_ID]
|
||||
)
|
||||
await cloud.async_delete_cloudhook(hass, entry.data[CONF_WEBHOOK_ID])
|
||||
except cloud.CloudNotAvailable:
|
||||
pass
|
||||
|
||||
|
||||
class OverseerrWebhookManager:
|
||||
"""Overseerr webhook manager."""
|
||||
|
||||
@ -86,6 +101,8 @@ class OverseerrWebhookManager:
|
||||
for url in urls:
|
||||
if url not in res:
|
||||
res.append(url)
|
||||
if CONF_CLOUDHOOK_URL in self.entry.data:
|
||||
res.append(self.entry.data[CONF_CLOUDHOOK_URL])
|
||||
return res
|
||||
|
||||
async def register_webhook(self) -> None:
|
||||
@ -101,16 +118,18 @@ class OverseerrWebhookManager:
|
||||
if not await self.check_need_change():
|
||||
return
|
||||
for url in self.webhook_urls:
|
||||
if await self.client.test_webhook_notification_config(url, JSON_PAYLOAD):
|
||||
LOGGER.debug("Setting Overseerr webhook to %s", url)
|
||||
await self.client.set_webhook_notification_config(
|
||||
enabled=True,
|
||||
types=REGISTERED_NOTIFICATIONS,
|
||||
webhook_url=url,
|
||||
json_payload=JSON_PAYLOAD,
|
||||
)
|
||||
if await self.test_and_set_webhook(url):
|
||||
return
|
||||
LOGGER.error("Failed to set Overseerr webhook")
|
||||
LOGGER.info("Failed to register Overseerr webhook")
|
||||
if (
|
||||
cloud.async_active_subscription(self.hass)
|
||||
and CONF_CLOUDHOOK_URL not in self.entry.data
|
||||
):
|
||||
LOGGER.info("Trying to register a cloudhook URL")
|
||||
url = await _async_cloudhook_generate_url(self.hass, self.entry)
|
||||
if await self.test_and_set_webhook(url):
|
||||
return
|
||||
LOGGER.error("Failed to register Overseerr cloudhook")
|
||||
|
||||
async def check_need_change(self) -> bool:
|
||||
"""Check if webhook needs to be changed."""
|
||||
@ -122,6 +141,19 @@ class OverseerrWebhookManager:
|
||||
or current_config.types != REGISTERED_NOTIFICATIONS
|
||||
)
|
||||
|
||||
async def test_and_set_webhook(self, url: str) -> bool:
|
||||
"""Test and set webhook."""
|
||||
if await self.client.test_webhook_notification_config(url, JSON_PAYLOAD):
|
||||
LOGGER.debug("Setting Overseerr webhook to %s", url)
|
||||
await self.client.set_webhook_notification_config(
|
||||
enabled=True,
|
||||
types=REGISTERED_NOTIFICATIONS,
|
||||
webhook_url=url,
|
||||
json_payload=JSON_PAYLOAD,
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
async def handle_webhook(
|
||||
self, hass: HomeAssistant, webhook_id: str, request: Request
|
||||
) -> Response:
|
||||
@ -136,3 +168,16 @@ class OverseerrWebhookManager:
|
||||
async def unregister_webhook(self) -> None:
|
||||
"""Unregister webhook."""
|
||||
async_unregister(self.hass, self.entry.data[CONF_WEBHOOK_ID])
|
||||
|
||||
|
||||
async def _async_cloudhook_generate_url(
|
||||
hass: HomeAssistant, entry: OverseerrConfigEntry
|
||||
) -> str:
|
||||
"""Generate the full URL for a webhook_id."""
|
||||
if CONF_CLOUDHOOK_URL not in entry.data:
|
||||
webhook_id = entry.data[CONF_WEBHOOK_ID]
|
||||
webhook_url = await cloud.async_create_cloudhook(hass, webhook_id)
|
||||
data = {**entry.data, CONF_CLOUDHOOK_URL: webhook_url}
|
||||
hass.config_entries.async_update_entry(entry, data=data)
|
||||
return webhook_url
|
||||
return cast(str, entry.data[CONF_CLOUDHOOK_URL])
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"domain": "overseerr",
|
||||
"name": "Overseerr",
|
||||
"after_dependencies": ["cloud"],
|
||||
"codeowners": ["@joostlek"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["http", "webhook"],
|
||||
|
@ -7,6 +7,7 @@ import pytest
|
||||
from python_overseerr import MovieDetails, RequestCount, RequestResponse
|
||||
from python_overseerr.models import TVDetails, WebhookNotificationConfig
|
||||
|
||||
from homeassistant.components.overseerr import CONF_CLOUDHOOK_URL
|
||||
from homeassistant.components.overseerr.const import DOMAIN
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
@ -66,6 +67,24 @@ def mock_overseerr_client() -> Generator[AsyncMock]:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_overseerr_client_needs_change(
|
||||
mock_overseerr_client: AsyncMock,
|
||||
) -> Generator[AsyncMock]:
|
||||
"""Mock an Overseerr client."""
|
||||
mock_overseerr_client.get_webhook_notification_config.return_value.types = 0
|
||||
return mock_overseerr_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_overseerr_client_cloudhook(
|
||||
mock_overseerr_client: AsyncMock,
|
||||
) -> Generator[AsyncMock]:
|
||||
"""Mock an Overseerr client."""
|
||||
mock_overseerr_client.get_webhook_notification_config.return_value.options.webhook_url = "https://hooks.nabu.casa/ABCD"
|
||||
return mock_overseerr_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock a config entry."""
|
||||
@ -81,3 +100,21 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
},
|
||||
entry_id="01JG00V55WEVTJ0CJHM0GAD7PC",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_cloudhook_config_entry() -> MockConfigEntry:
|
||||
"""Mock a config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="Overseerr",
|
||||
data={
|
||||
CONF_HOST: "overseerr.test",
|
||||
CONF_PORT: 80,
|
||||
CONF_SSL: False,
|
||||
CONF_API_KEY: "test-key",
|
||||
CONF_WEBHOOK_ID: WEBHOOK_ID,
|
||||
CONF_CLOUDHOOK_URL: "https://hooks.nabu.casa/ABCD",
|
||||
},
|
||||
entry_id="01JG00V55WEVTJ0CJHM0GAD7PC",
|
||||
)
|
||||
|
@ -1,13 +1,18 @@
|
||||
"""Tests for the Overseerr integration."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from python_overseerr.models import WebhookNotificationOptions
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.overseerr import JSON_PAYLOAD, REGISTERED_NOTIFICATIONS
|
||||
from homeassistant.components import cloud
|
||||
from homeassistant.components.overseerr import (
|
||||
CONF_CLOUDHOOK_URL,
|
||||
JSON_PAYLOAD,
|
||||
REGISTERED_NOTIFICATIONS,
|
||||
)
|
||||
from homeassistant.components.overseerr.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
@ -15,6 +20,7 @@ from homeassistant.helpers import device_registry as dr
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.cloud import mock_cloud
|
||||
|
||||
|
||||
async def test_device_info(
|
||||
@ -150,3 +156,192 @@ async def test_prefer_internal_ip(
|
||||
mock_overseerr_client.test_webhook_notification_config.call_args_list[1][0][0]
|
||||
== "https://www.example.com/api/webhook/test-webhook-id"
|
||||
)
|
||||
|
||||
|
||||
async def test_cloudhook_setup(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_overseerr_client_needs_change: AsyncMock,
|
||||
) -> None:
|
||||
"""Test if set up with active cloud subscription and cloud hook."""
|
||||
|
||||
await mock_cloud(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_overseerr_client_needs_change.test_webhook_notification_config.side_effect = [
|
||||
False,
|
||||
True,
|
||||
]
|
||||
|
||||
with (
|
||||
patch("homeassistant.components.cloud.async_is_logged_in", return_value=True),
|
||||
patch("homeassistant.components.cloud.async_is_connected", return_value=True),
|
||||
patch.object(cloud, "async_active_subscription", return_value=True),
|
||||
patch(
|
||||
"homeassistant.components.cloud.async_create_cloudhook",
|
||||
return_value="https://hooks.nabu.casa/ABCD",
|
||||
) as fake_create_cloudhook,
|
||||
patch(
|
||||
"homeassistant.components.cloud.async_delete_cloudhook"
|
||||
) as fake_delete_cloudhook,
|
||||
):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert cloud.async_active_subscription(hass) is True
|
||||
|
||||
assert (
|
||||
mock_config_entry.data[CONF_CLOUDHOOK_URL] == "https://hooks.nabu.casa/ABCD"
|
||||
)
|
||||
|
||||
assert (
|
||||
len(
|
||||
mock_overseerr_client_needs_change.test_webhook_notification_config.mock_calls
|
||||
)
|
||||
== 2
|
||||
)
|
||||
|
||||
assert hass.config_entries.async_entries(DOMAIN)
|
||||
fake_create_cloudhook.assert_called()
|
||||
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN):
|
||||
await hass.config_entries.async_remove(config_entry.entry_id)
|
||||
fake_delete_cloudhook.assert_called_once()
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert not hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
|
||||
async def test_cloudhook_consistent(
|
||||
hass: HomeAssistant,
|
||||
mock_cloudhook_config_entry: MockConfigEntry,
|
||||
mock_overseerr_client_needs_change: AsyncMock,
|
||||
) -> None:
|
||||
"""Test if we keep the cloudhook if it is already set up."""
|
||||
|
||||
await mock_cloud(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_overseerr_client_needs_change.test_webhook_notification_config.side_effect = [
|
||||
False,
|
||||
True,
|
||||
]
|
||||
|
||||
with (
|
||||
patch("homeassistant.components.cloud.async_is_logged_in", return_value=True),
|
||||
patch("homeassistant.components.cloud.async_is_connected", return_value=True),
|
||||
patch.object(cloud, "async_active_subscription", return_value=True),
|
||||
patch(
|
||||
"homeassistant.components.cloud.async_create_cloudhook",
|
||||
return_value="https://hooks.nabu.casa/ABCD",
|
||||
) as fake_create_cloudhook,
|
||||
):
|
||||
await setup_integration(hass, mock_cloudhook_config_entry)
|
||||
|
||||
assert cloud.async_active_subscription(hass) is True
|
||||
|
||||
assert (
|
||||
mock_cloudhook_config_entry.data[CONF_CLOUDHOOK_URL]
|
||||
== "https://hooks.nabu.casa/ABCD"
|
||||
)
|
||||
|
||||
assert (
|
||||
len(
|
||||
mock_overseerr_client_needs_change.test_webhook_notification_config.mock_calls
|
||||
)
|
||||
== 2
|
||||
)
|
||||
|
||||
assert hass.config_entries.async_entries(DOMAIN)
|
||||
fake_create_cloudhook.assert_not_called()
|
||||
|
||||
|
||||
async def test_cloudhook_needs_no_change(
|
||||
hass: HomeAssistant,
|
||||
mock_cloudhook_config_entry: MockConfigEntry,
|
||||
mock_overseerr_client_cloudhook: AsyncMock,
|
||||
) -> None:
|
||||
"""Test if we keep the cloudhook if it is already set up."""
|
||||
|
||||
await setup_integration(hass, mock_cloudhook_config_entry)
|
||||
|
||||
assert (
|
||||
len(mock_overseerr_client_cloudhook.test_webhook_notification_config.mock_calls)
|
||||
== 0
|
||||
)
|
||||
|
||||
|
||||
async def test_cloudhook_not_needed(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_overseerr_client_needs_change: AsyncMock,
|
||||
) -> None:
|
||||
"""Test if we prefer local webhook over cloudhook."""
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with (
|
||||
patch.object(cloud, "async_active_subscription", return_value=True),
|
||||
):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert cloud.async_active_subscription(hass) is True
|
||||
|
||||
assert CONF_CLOUDHOOK_URL not in mock_config_entry.data
|
||||
|
||||
assert (
|
||||
len(
|
||||
mock_overseerr_client_needs_change.test_webhook_notification_config.mock_calls
|
||||
)
|
||||
== 1
|
||||
)
|
||||
assert (
|
||||
mock_overseerr_client_needs_change.test_webhook_notification_config.call_args_list[
|
||||
0
|
||||
][0][0]
|
||||
== "http://10.10.10.10:8123/api/webhook/test-webhook-id"
|
||||
)
|
||||
|
||||
|
||||
async def test_cloudhook_not_connecting(
|
||||
hass: HomeAssistant,
|
||||
mock_cloudhook_config_entry: MockConfigEntry,
|
||||
mock_overseerr_client_needs_change: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the cloudhook is not registered if Overseerr cannot connect to it."""
|
||||
|
||||
await mock_cloud(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_overseerr_client_needs_change.test_webhook_notification_config.return_value = (
|
||||
False
|
||||
)
|
||||
|
||||
with (
|
||||
patch("homeassistant.components.cloud.async_is_logged_in", return_value=True),
|
||||
patch("homeassistant.components.cloud.async_is_connected", return_value=True),
|
||||
patch.object(cloud, "async_active_subscription", return_value=True),
|
||||
patch(
|
||||
"homeassistant.components.cloud.async_create_cloudhook",
|
||||
return_value="https://hooks.nabu.casa/ABCD",
|
||||
) as fake_create_cloudhook,
|
||||
):
|
||||
await setup_integration(hass, mock_cloudhook_config_entry)
|
||||
|
||||
assert cloud.async_active_subscription(hass) is True
|
||||
|
||||
assert (
|
||||
mock_cloudhook_config_entry.data[CONF_CLOUDHOOK_URL]
|
||||
== "https://hooks.nabu.casa/ABCD"
|
||||
)
|
||||
|
||||
assert (
|
||||
len(
|
||||
mock_overseerr_client_needs_change.test_webhook_notification_config.mock_calls
|
||||
)
|
||||
== 2
|
||||
)
|
||||
|
||||
mock_overseerr_client_needs_change.set_webhook_notification_config.assert_not_called()
|
||||
|
||||
assert hass.config_entries.async_entries(DOMAIN)
|
||||
fake_create_cloudhook.assert_not_called()
|
||||
|
Loading…
x
Reference in New Issue
Block a user