mirror of
https://github.com/home-assistant/core.git
synced 2025-12-20 02:48:57 +00:00
Add issue sensors to Overseerr integration (#158888)
This commit is contained in:
@@ -159,7 +159,8 @@ class OverseerrWebhookManager:
|
|||||||
"""Handle webhook."""
|
"""Handle webhook."""
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
LOGGER.debug("Received webhook payload: %s", data)
|
LOGGER.debug("Received webhook payload: %s", data)
|
||||||
if data["notification_type"].startswith("MEDIA"):
|
notification_type = data["notification_type"]
|
||||||
|
if notification_type.startswith(("REQUEST_", "ISSUE_", "MEDIA_")):
|
||||||
await self.entry.runtime_data.async_refresh()
|
await self.entry.runtime_data.async_refresh()
|
||||||
async_dispatcher_send(hass, EVENT_KEY, data)
|
async_dispatcher_send(hass, EVENT_KEY, data)
|
||||||
return HomeAssistantView.json({"message": "ok"})
|
return HomeAssistantView.json({"message": "ok"})
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ REGISTERED_NOTIFICATIONS = (
|
|||||||
| NotificationType.REQUEST_AVAILABLE
|
| NotificationType.REQUEST_AVAILABLE
|
||||||
| NotificationType.REQUEST_PROCESSING_FAILED
|
| NotificationType.REQUEST_PROCESSING_FAILED
|
||||||
| NotificationType.REQUEST_AUTOMATICALLY_APPROVED
|
| NotificationType.REQUEST_AUTOMATICALLY_APPROVED
|
||||||
|
| NotificationType.ISSUE_REPORTED
|
||||||
|
| NotificationType.ISSUE_COMMENTED
|
||||||
|
| NotificationType.ISSUE_RESOLVED
|
||||||
|
| NotificationType.ISSUE_REOPENED
|
||||||
)
|
)
|
||||||
JSON_PAYLOAD = (
|
JSON_PAYLOAD = (
|
||||||
'"{\\"notification_type\\":\\"{{notification_type}}\\",\\"subject\\":\\"{{subject}'
|
'"{\\"notification_type\\":\\"{{notification_type}}\\",\\"subject\\":\\"{{subject}'
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from python_overseerr import (
|
|||||||
OverseerrAuthenticationError,
|
OverseerrAuthenticationError,
|
||||||
OverseerrClient,
|
OverseerrClient,
|
||||||
OverseerrConnectionError,
|
OverseerrConnectionError,
|
||||||
RequestCount,
|
|
||||||
)
|
)
|
||||||
from yarl import URL
|
from yarl import URL
|
||||||
|
|
||||||
@@ -18,11 +17,12 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DOMAIN, LOGGER
|
from .const import DOMAIN, LOGGER
|
||||||
|
from .models import OverseerrData
|
||||||
|
|
||||||
type OverseerrConfigEntry = ConfigEntry[OverseerrCoordinator]
|
type OverseerrConfigEntry = ConfigEntry[OverseerrCoordinator]
|
||||||
|
|
||||||
|
|
||||||
class OverseerrCoordinator(DataUpdateCoordinator[RequestCount]):
|
class OverseerrCoordinator(DataUpdateCoordinator[OverseerrData]):
|
||||||
"""Class to manage fetching Overseerr data."""
|
"""Class to manage fetching Overseerr data."""
|
||||||
|
|
||||||
config_entry: OverseerrConfigEntry
|
config_entry: OverseerrConfigEntry
|
||||||
@@ -49,10 +49,12 @@ class OverseerrCoordinator(DataUpdateCoordinator[RequestCount]):
|
|||||||
self.url = URL.build(host=host, port=port, scheme="https" if ssl else "http")
|
self.url = URL.build(host=host, port=port, scheme="https" if ssl else "http")
|
||||||
self.push = False
|
self.push = False
|
||||||
|
|
||||||
async def _async_update_data(self) -> RequestCount:
|
async def _async_update_data(self) -> OverseerrData:
|
||||||
"""Fetch data from API endpoint."""
|
"""Fetch data from API endpoint."""
|
||||||
try:
|
try:
|
||||||
return await self.client.get_request_count()
|
requests = await self.client.get_request_count()
|
||||||
|
issues = await self.client.get_issue_count()
|
||||||
|
return OverseerrData(requests=requests, issues=issues)
|
||||||
except OverseerrAuthenticationError as err:
|
except OverseerrAuthenticationError as err:
|
||||||
raise ConfigEntryAuthFailed(
|
raise ConfigEntryAuthFailed(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
|
|||||||
13
homeassistant/components/overseerr/models.py
Normal file
13
homeassistant/components/overseerr/models.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
"""Data models for Overseerr integration."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from python_overseerr import IssueCount, RequestCount
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OverseerrData:
|
||||||
|
"""Data model for Overseerr coordinator."""
|
||||||
|
|
||||||
|
requests: RequestCount
|
||||||
|
issues: IssueCount
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from python_overseerr import RequestCount
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
@@ -16,6 +14,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|||||||
from .const import REQUESTS
|
from .const import REQUESTS
|
||||||
from .coordinator import OverseerrConfigEntry, OverseerrCoordinator
|
from .coordinator import OverseerrConfigEntry, OverseerrCoordinator
|
||||||
from .entity import OverseerrEntity
|
from .entity import OverseerrEntity
|
||||||
|
from .models import OverseerrData
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ PARALLEL_UPDATES = 0
|
|||||||
class OverseerrSensorEntityDescription(SensorEntityDescription):
|
class OverseerrSensorEntityDescription(SensorEntityDescription):
|
||||||
"""Describes Overseerr config sensor entity."""
|
"""Describes Overseerr config sensor entity."""
|
||||||
|
|
||||||
value_fn: Callable[[RequestCount], int]
|
value_fn: Callable[[OverseerrData], int]
|
||||||
|
|
||||||
|
|
||||||
SENSORS: tuple[OverseerrSensorEntityDescription, ...] = (
|
SENSORS: tuple[OverseerrSensorEntityDescription, ...] = (
|
||||||
@@ -32,43 +31,73 @@ SENSORS: tuple[OverseerrSensorEntityDescription, ...] = (
|
|||||||
key="total_requests",
|
key="total_requests",
|
||||||
native_unit_of_measurement=REQUESTS,
|
native_unit_of_measurement=REQUESTS,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
value_fn=lambda count: count.total,
|
value_fn=lambda data: data.requests.total,
|
||||||
),
|
),
|
||||||
OverseerrSensorEntityDescription(
|
OverseerrSensorEntityDescription(
|
||||||
key="movie_requests",
|
key="movie_requests",
|
||||||
native_unit_of_measurement=REQUESTS,
|
native_unit_of_measurement=REQUESTS,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
value_fn=lambda count: count.movie,
|
value_fn=lambda data: data.requests.movie,
|
||||||
),
|
),
|
||||||
OverseerrSensorEntityDescription(
|
OverseerrSensorEntityDescription(
|
||||||
key="tv_requests",
|
key="tv_requests",
|
||||||
native_unit_of_measurement=REQUESTS,
|
native_unit_of_measurement=REQUESTS,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
value_fn=lambda count: count.tv,
|
value_fn=lambda data: data.requests.tv,
|
||||||
),
|
),
|
||||||
OverseerrSensorEntityDescription(
|
OverseerrSensorEntityDescription(
|
||||||
key="pending_requests",
|
key="pending_requests",
|
||||||
native_unit_of_measurement=REQUESTS,
|
native_unit_of_measurement=REQUESTS,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
value_fn=lambda count: count.pending,
|
value_fn=lambda data: data.requests.pending,
|
||||||
),
|
),
|
||||||
OverseerrSensorEntityDescription(
|
OverseerrSensorEntityDescription(
|
||||||
key="declined_requests",
|
key="declined_requests",
|
||||||
native_unit_of_measurement=REQUESTS,
|
native_unit_of_measurement=REQUESTS,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
value_fn=lambda count: count.declined,
|
value_fn=lambda data: data.requests.declined,
|
||||||
),
|
),
|
||||||
OverseerrSensorEntityDescription(
|
OverseerrSensorEntityDescription(
|
||||||
key="processing_requests",
|
key="processing_requests",
|
||||||
native_unit_of_measurement=REQUESTS,
|
native_unit_of_measurement=REQUESTS,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
value_fn=lambda count: count.processing,
|
value_fn=lambda data: data.requests.processing,
|
||||||
),
|
),
|
||||||
OverseerrSensorEntityDescription(
|
OverseerrSensorEntityDescription(
|
||||||
key="available_requests",
|
key="available_requests",
|
||||||
native_unit_of_measurement=REQUESTS,
|
native_unit_of_measurement=REQUESTS,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
value_fn=lambda count: count.available,
|
value_fn=lambda data: data.requests.available,
|
||||||
|
),
|
||||||
|
OverseerrSensorEntityDescription(
|
||||||
|
key="total_issues",
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
value_fn=lambda data: data.issues.total,
|
||||||
|
),
|
||||||
|
OverseerrSensorEntityDescription(
|
||||||
|
key="open_issues",
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
value_fn=lambda data: data.issues.open,
|
||||||
|
),
|
||||||
|
OverseerrSensorEntityDescription(
|
||||||
|
key="closed_issues",
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
value_fn=lambda data: data.issues.closed,
|
||||||
|
),
|
||||||
|
OverseerrSensorEntityDescription(
|
||||||
|
key="video_issues",
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
value_fn=lambda data: data.issues.video,
|
||||||
|
),
|
||||||
|
OverseerrSensorEntityDescription(
|
||||||
|
key="audio_issues",
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
value_fn=lambda data: data.issues.audio,
|
||||||
|
),
|
||||||
|
OverseerrSensorEntityDescription(
|
||||||
|
key="subtitle_issues",
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
value_fn=lambda data: data.issues.subtitles,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -50,26 +50,62 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
|
"audio_issues": {
|
||||||
|
"name": "Audio issues",
|
||||||
|
"state": {
|
||||||
|
"measurement": "issues"
|
||||||
|
}
|
||||||
|
},
|
||||||
"available_requests": {
|
"available_requests": {
|
||||||
"name": "Available requests"
|
"name": "Available requests"
|
||||||
},
|
},
|
||||||
|
"closed_issues": {
|
||||||
|
"name": "Closed issues",
|
||||||
|
"state": {
|
||||||
|
"measurement": "issues"
|
||||||
|
}
|
||||||
|
},
|
||||||
"declined_requests": {
|
"declined_requests": {
|
||||||
"name": "Declined requests"
|
"name": "Declined requests"
|
||||||
},
|
},
|
||||||
"movie_requests": {
|
"movie_requests": {
|
||||||
"name": "Movie requests"
|
"name": "Movie requests"
|
||||||
},
|
},
|
||||||
|
"open_issues": {
|
||||||
|
"name": "Open issues",
|
||||||
|
"state": {
|
||||||
|
"measurement": "issues"
|
||||||
|
}
|
||||||
|
},
|
||||||
"pending_requests": {
|
"pending_requests": {
|
||||||
"name": "Pending requests"
|
"name": "Pending requests"
|
||||||
},
|
},
|
||||||
"processing_requests": {
|
"processing_requests": {
|
||||||
"name": "Processing requests"
|
"name": "Processing requests"
|
||||||
},
|
},
|
||||||
|
"subtitle_issues": {
|
||||||
|
"name": "Subtitle issues",
|
||||||
|
"state": {
|
||||||
|
"measurement": "issues"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"total_issues": {
|
||||||
|
"name": "Total issues",
|
||||||
|
"state": {
|
||||||
|
"measurement": "issues"
|
||||||
|
}
|
||||||
|
},
|
||||||
"total_requests": {
|
"total_requests": {
|
||||||
"name": "Total requests"
|
"name": "Total requests"
|
||||||
},
|
},
|
||||||
"tv_requests": {
|
"tv_requests": {
|
||||||
"name": "TV requests"
|
"name": "TV requests"
|
||||||
|
},
|
||||||
|
"video_issues": {
|
||||||
|
"name": "Video issues",
|
||||||
|
"state": {
|
||||||
|
"measurement": "issues"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from collections.abc import Generator
|
|||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from python_overseerr import MovieDetails, RequestCount, RequestResponse
|
from python_overseerr import IssueCount, MovieDetails, RequestCount, RequestResponse
|
||||||
from python_overseerr.models import TVDetails, WebhookNotificationConfig
|
from python_overseerr.models import TVDetails, WebhookNotificationConfig
|
||||||
|
|
||||||
from homeassistant.components.overseerr import CONF_CLOUDHOOK_URL
|
from homeassistant.components.overseerr import CONF_CLOUDHOOK_URL
|
||||||
@@ -49,6 +49,9 @@ def mock_overseerr_client() -> Generator[AsyncMock]:
|
|||||||
client.get_request_count.return_value = RequestCount.from_json(
|
client.get_request_count.return_value = RequestCount.from_json(
|
||||||
load_fixture("request_count.json", DOMAIN)
|
load_fixture("request_count.json", DOMAIN)
|
||||||
)
|
)
|
||||||
|
client.get_issue_count.return_value = IssueCount.from_json(
|
||||||
|
load_fixture("issue_count.json", DOMAIN)
|
||||||
|
)
|
||||||
client.get_webhook_notification_config.return_value = (
|
client.get_webhook_notification_config.return_value = (
|
||||||
WebhookNotificationConfig.from_json(
|
WebhookNotificationConfig.from_json(
|
||||||
load_fixture("webhook_config.json", DOMAIN)
|
load_fixture("webhook_config.json", DOMAIN)
|
||||||
|
|||||||
9
tests/components/overseerr/fixtures/issue_count.json
Normal file
9
tests/components/overseerr/fixtures/issue_count.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"total": 15,
|
||||||
|
"video": 6,
|
||||||
|
"audio": 4,
|
||||||
|
"subtitles": 3,
|
||||||
|
"others": 2,
|
||||||
|
"open": 10,
|
||||||
|
"closed": 5
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"types": 222,
|
"types": 4062,
|
||||||
"options": {
|
"options": {
|
||||||
"jsonPayload": "{\"notification_type\":\"{{notification_type}}\",\"subject\":\"{{subject}}\",\"message\":\"{{message}}\",\"image\":\"{{image}}\",\"{{media}}\":{\"media_type\":\"{{media_type}}\",\"tmdb_id\":\"{{media_tmdbid}}\",\"tvdb_id\":\"{{media_tvdbid}}\",\"status\":\"{{media_status}}\",\"status4k\":\"{{media_status4k}}\"},\"{{request}}\":{\"request_id\":\"{{request_id}}\",\"requested_by_email\":\"{{requestedBy_email}}\",\"requested_by_username\":\"{{requestedBy_username}}\",\"requested_by_avatar\":\"{{requestedBy_avatar}}\",\"requested_by_settings_discord_id\":\"{{requestedBy_settings_discordId}}\",\"requested_by_settings_telegram_chat_id\":\"{{requestedBy_settings_telegramChatId}}\"},\"{{issue}}\":{\"issue_id\":\"{{issue_id}}\",\"issue_type\":\"{{issue_type}}\",\"issue_status\":\"{{issue_status}}\",\"reported_by_email\":\"{{reportedBy_email}}\",\"reported_by_username\":\"{{reportedBy_username}}\",\"reported_by_avatar\":\"{{reportedBy_avatar}}\",\"reported_by_settings_discord_id\":\"{{reportedBy_settings_discordId}}\",\"reported_by_settings_telegram_chat_id\":\"{{reportedBy_settings_telegramChatId}}\"},\"{{comment}}\":{\"comment_message\":\"{{comment_message}}\",\"commented_by_email\":\"{{commentedBy_email}}\",\"commented_by_username\":\"{{commentedBy_username}}\",\"commented_by_avatar\":\"{{commentedBy_avatar}}\",\"commented_by_settings_discord_id\":\"{{commentedBy_settings_discordId}}\",\"commented_by_settings_telegram_chat_id\":\"{{commentedBy_settings_telegramChatId}}\"}}",
|
"jsonPayload": "{\"notification_type\":\"{{notification_type}}\",\"subject\":\"{{subject}}\",\"message\":\"{{message}}\",\"image\":\"{{image}}\",\"{{media}}\":{\"media_type\":\"{{media_type}}\",\"tmdb_id\":\"{{media_tmdbid}}\",\"tvdb_id\":\"{{media_tvdbid}}\",\"status\":\"{{media_status}}\",\"status4k\":\"{{media_status4k}}\"},\"{{request}}\":{\"request_id\":\"{{request_id}}\",\"requested_by_email\":\"{{requestedBy_email}}\",\"requested_by_username\":\"{{requestedBy_username}}\",\"requested_by_avatar\":\"{{requestedBy_avatar}}\",\"requested_by_settings_discord_id\":\"{{requestedBy_settings_discordId}}\",\"requested_by_settings_telegram_chat_id\":\"{{requestedBy_settings_telegramChatId}}\"},\"{{issue}}\":{\"issue_id\":\"{{issue_id}}\",\"issue_type\":\"{{issue_type}}\",\"issue_status\":\"{{issue_status}}\",\"reported_by_email\":\"{{reportedBy_email}}\",\"reported_by_username\":\"{{reportedBy_username}}\",\"reported_by_avatar\":\"{{reportedBy_avatar}}\",\"reported_by_settings_discord_id\":\"{{reportedBy_settings_discordId}}\",\"reported_by_settings_telegram_chat_id\":\"{{reportedBy_settings_telegramChatId}}\"},\"{{comment}}\":{\"comment_message\":\"{{comment_message}}\",\"commented_by_email\":\"{{commentedBy_email}}\",\"commented_by_username\":\"{{commentedBy_username}}\",\"commented_by_avatar\":\"{{commentedBy_avatar}}\",\"commented_by_settings_discord_id\":\"{{commentedBy_settings_discordId}}\",\"commented_by_settings_telegram_chat_id\":\"{{commentedBy_settings_telegramChatId}}\"}}",
|
||||||
"webhookUrl": "http://10.10.10.10:8123/api/webhook/test-webhook-id"
|
"webhookUrl": "http://10.10.10.10:8123/api/webhook/test-webhook-id"
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"notification_type": "ISSUE_REPORTED",
|
||||||
|
"subject": "New Issue Reported",
|
||||||
|
"message": "A new video issue has been reported for Interstellar",
|
||||||
|
"image": "https://image.tmdb.org/t/p/w600_and_h900_bestv2/gEU2QniE6E77NI6lCU6MxlNBvIx.jpg",
|
||||||
|
"media": {
|
||||||
|
"media_type": "movie",
|
||||||
|
"tmdb_id": "157336",
|
||||||
|
"tvdb_id": "",
|
||||||
|
"status": "available",
|
||||||
|
"status4k": "unknown"
|
||||||
|
},
|
||||||
|
"issue": {
|
||||||
|
"issue_id": "1",
|
||||||
|
"issue_type": "video",
|
||||||
|
"issue_status": "open",
|
||||||
|
"reported_by_email": "user@example.com",
|
||||||
|
"reported_by_username": "testuser",
|
||||||
|
"reported_by_avatar": "/os_logo_square.png",
|
||||||
|
"reported_by_settings_discord_id": "",
|
||||||
|
"reported_by_settings_telegram_chat_id": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,14 +2,25 @@
|
|||||||
# name: test_diagnostics_polling_instance
|
# name: test_diagnostics_polling_instance
|
||||||
dict({
|
dict({
|
||||||
'coordinator_data': dict({
|
'coordinator_data': dict({
|
||||||
'approved': 11,
|
'issues': dict({
|
||||||
'available': 8,
|
'audio': 4,
|
||||||
'declined': 0,
|
'closed': 5,
|
||||||
'movie': 9,
|
'open': 10,
|
||||||
'pending': 0,
|
'others': 2,
|
||||||
'processing': 3,
|
'subtitles': 3,
|
||||||
'total': 11,
|
'total': 15,
|
||||||
'tv': 2,
|
'video': 6,
|
||||||
|
}),
|
||||||
|
'requests': dict({
|
||||||
|
'approved': 11,
|
||||||
|
'available': 8,
|
||||||
|
'declined': 0,
|
||||||
|
'movie': 9,
|
||||||
|
'pending': 0,
|
||||||
|
'processing': 3,
|
||||||
|
'total': 11,
|
||||||
|
'tv': 2,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
'has_cloudhooks': False,
|
'has_cloudhooks': False,
|
||||||
})
|
})
|
||||||
@@ -17,14 +28,25 @@
|
|||||||
# name: test_diagnostics_webhook_instance
|
# name: test_diagnostics_webhook_instance
|
||||||
dict({
|
dict({
|
||||||
'coordinator_data': dict({
|
'coordinator_data': dict({
|
||||||
'approved': 11,
|
'issues': dict({
|
||||||
'available': 8,
|
'audio': 4,
|
||||||
'declined': 0,
|
'closed': 5,
|
||||||
'movie': 9,
|
'open': 10,
|
||||||
'pending': 0,
|
'others': 2,
|
||||||
'processing': 3,
|
'subtitles': 3,
|
||||||
'total': 11,
|
'total': 15,
|
||||||
'tv': 2,
|
'video': 6,
|
||||||
|
}),
|
||||||
|
'requests': dict({
|
||||||
|
'approved': 11,
|
||||||
|
'available': 8,
|
||||||
|
'declined': 0,
|
||||||
|
'movie': 9,
|
||||||
|
'pending': 0,
|
||||||
|
'processing': 3,
|
||||||
|
'total': 11,
|
||||||
|
'tv': 2,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
'has_cloudhooks': True,
|
'has_cloudhooks': True,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,55 @@
|
|||||||
# serializer version: 1
|
# serializer version: 1
|
||||||
|
# name: test_all_entities[sensor.overseerr_audio_issues-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.overseerr_audio_issues',
|
||||||
|
'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': 'Audio issues',
|
||||||
|
'platform': 'overseerr',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'audio_issues',
|
||||||
|
'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-audio_issues',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.overseerr_audio_issues-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Overseerr Audio issues',
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.overseerr_audio_issues',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '4',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_all_entities[sensor.overseerr_available_requests-entry]
|
# name: test_all_entities[sensor.overseerr_available_requests-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
@@ -51,6 +102,57 @@
|
|||||||
'state': '8',
|
'state': '8',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_all_entities[sensor.overseerr_closed_issues-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.overseerr_closed_issues',
|
||||||
|
'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': 'Closed issues',
|
||||||
|
'platform': 'overseerr',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'closed_issues',
|
||||||
|
'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-closed_issues',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.overseerr_closed_issues-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Overseerr Closed issues',
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.overseerr_closed_issues',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '5',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_all_entities[sensor.overseerr_declined_requests-entry]
|
# name: test_all_entities[sensor.overseerr_declined_requests-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
@@ -155,6 +257,57 @@
|
|||||||
'state': '9',
|
'state': '9',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_all_entities[sensor.overseerr_open_issues-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.overseerr_open_issues',
|
||||||
|
'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': 'Open issues',
|
||||||
|
'platform': 'overseerr',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'open_issues',
|
||||||
|
'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-open_issues',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.overseerr_open_issues-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Overseerr Open issues',
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.overseerr_open_issues',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '10',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_all_entities[sensor.overseerr_pending_requests-entry]
|
# name: test_all_entities[sensor.overseerr_pending_requests-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
@@ -259,6 +412,108 @@
|
|||||||
'state': '3',
|
'state': '3',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_all_entities[sensor.overseerr_subtitle_issues-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.overseerr_subtitle_issues',
|
||||||
|
'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': 'Subtitle issues',
|
||||||
|
'platform': 'overseerr',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'subtitle_issues',
|
||||||
|
'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-subtitle_issues',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.overseerr_subtitle_issues-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Overseerr Subtitle issues',
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.overseerr_subtitle_issues',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '3',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.overseerr_total_issues-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.overseerr_total_issues',
|
||||||
|
'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': 'Total issues',
|
||||||
|
'platform': 'overseerr',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'total_issues',
|
||||||
|
'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-total_issues',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.overseerr_total_issues-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Overseerr Total issues',
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.overseerr_total_issues',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '15',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_all_entities[sensor.overseerr_total_requests-entry]
|
# name: test_all_entities[sensor.overseerr_total_requests-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
@@ -363,3 +618,54 @@
|
|||||||
'state': '2',
|
'state': '2',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_all_entities[sensor.overseerr_video_issues-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.overseerr_video_issues',
|
||||||
|
'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': 'Video issues',
|
||||||
|
'platform': 'overseerr',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'video_issues',
|
||||||
|
'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-video_issues',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.overseerr_video_issues-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Overseerr Video issues',
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.overseerr_video_issues',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '6',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ async def test_proper_webhook_configuration(
|
|||||||
"""Test the webhook configuration."""
|
"""Test the webhook configuration."""
|
||||||
await setup_integration(hass, mock_config_entry)
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
assert REGISTERED_NOTIFICATIONS == 222
|
assert REGISTERED_NOTIFICATIONS == 4062
|
||||||
|
|
||||||
mock_overseerr_client.test_webhook_notification_config.assert_not_called()
|
mock_overseerr_client.test_webhook_notification_config.assert_not_called()
|
||||||
mock_overseerr_client.set_webhook_notification_config.assert_not_called()
|
mock_overseerr_client.set_webhook_notification_config.assert_not_called()
|
||||||
@@ -83,7 +83,6 @@ async def test_proper_webhook_configuration(
|
|||||||
[
|
[
|
||||||
{"return_value.enabled": False},
|
{"return_value.enabled": False},
|
||||||
{"return_value.types": 4},
|
{"return_value.types": 4},
|
||||||
{"return_value.types": 4062},
|
|
||||||
{
|
{
|
||||||
"return_value.options": WebhookNotificationOptions(
|
"return_value.options": WebhookNotificationOptions(
|
||||||
webhook_url="http://example.com", json_payload=JSON_PAYLOAD
|
webhook_url="http://example.com", json_payload=JSON_PAYLOAD
|
||||||
@@ -99,7 +98,6 @@ async def test_proper_webhook_configuration(
|
|||||||
ids=[
|
ids=[
|
||||||
"Disabled",
|
"Disabled",
|
||||||
"Smaller scope",
|
"Smaller scope",
|
||||||
"Bigger scope",
|
|
||||||
"Webhook URL",
|
"Webhook URL",
|
||||||
"JSON Payload",
|
"JSON Payload",
|
||||||
],
|
],
|
||||||
@@ -124,7 +122,6 @@ async def test_webhook_configuration_need_update(
|
|||||||
[
|
[
|
||||||
{"return_value.enabled": False},
|
{"return_value.enabled": False},
|
||||||
{"return_value.types": 4},
|
{"return_value.types": 4},
|
||||||
{"return_value.types": 4062},
|
|
||||||
{
|
{
|
||||||
"return_value.options": WebhookNotificationOptions(
|
"return_value.options": WebhookNotificationOptions(
|
||||||
webhook_url="http://example.com", json_payload=JSON_PAYLOAD
|
webhook_url="http://example.com", json_payload=JSON_PAYLOAD
|
||||||
@@ -140,7 +137,6 @@ async def test_webhook_configuration_need_update(
|
|||||||
ids=[
|
ids=[
|
||||||
"Disabled",
|
"Disabled",
|
||||||
"Smaller scope",
|
"Smaller scope",
|
||||||
"Bigger scope",
|
|
||||||
"Webhook URL",
|
"Webhook URL",
|
||||||
"JSON Payload",
|
"JSON Payload",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ async def test_webhook_trigger_update(
|
|||||||
mock_config_entry: MockConfigEntry,
|
mock_config_entry: MockConfigEntry,
|
||||||
hass_client_no_auth: ClientSessionGenerator,
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test all entities."""
|
"""Test webhook triggers coordinator update for request sensors."""
|
||||||
await setup_integration(hass, mock_config_entry)
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
assert hass.states.get("sensor.overseerr_available_requests").state == "8"
|
assert hass.states.get("sensor.overseerr_available_requests").state == "8"
|
||||||
@@ -57,3 +57,35 @@ async def test_webhook_trigger_update(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert hass.states.get("sensor.overseerr_available_requests").state == "7"
|
assert hass.states.get("sensor.overseerr_available_requests").state == "7"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_webhook_issue_trigger_update(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_overseerr_client: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
|
) -> None:
|
||||||
|
"""Test webhook triggers coordinator update for issue sensors."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.overseerr_total_issues").state == "15"
|
||||||
|
assert hass.states.get("sensor.overseerr_open_issues").state == "10"
|
||||||
|
assert hass.states.get("sensor.overseerr_video_issues").state == "6"
|
||||||
|
|
||||||
|
mock_overseerr_client.get_issue_count.return_value.total = 16
|
||||||
|
mock_overseerr_client.get_issue_count.return_value.open = 11
|
||||||
|
mock_overseerr_client.get_issue_count.return_value.video = 7
|
||||||
|
client = await hass_client_no_auth()
|
||||||
|
|
||||||
|
await call_webhook(
|
||||||
|
hass,
|
||||||
|
await async_load_json_object_fixture(
|
||||||
|
hass, "webhook_issue_reported.json", DOMAIN
|
||||||
|
),
|
||||||
|
client,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.overseerr_total_issues").state == "16"
|
||||||
|
assert hass.states.get("sensor.overseerr_open_issues").state == "11"
|
||||||
|
assert hass.states.get("sensor.overseerr_video_issues").state == "7"
|
||||||
|
|||||||
Reference in New Issue
Block a user