Move Tautulli attributes to their own sensors (#71712)

This commit is contained in:
Robert Hillis 2022-06-29 05:21:01 -04:00 committed by GitHub
parent 981249d330
commit 500105fa86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 247 additions and 50 deletions

View File

@ -1,7 +1,7 @@
"""The Tautulli integration."""
from __future__ import annotations
from pytautulli import PyTautulli, PyTautulliHostConfiguration
from pytautulli import PyTautulli, PyTautulliApiUser, PyTautulliHostConfiguration
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL, Platform
@ -50,14 +50,18 @@ class TautulliEntity(CoordinatorEntity[TautulliDataUpdateCoordinator]):
self,
coordinator: TautulliDataUpdateCoordinator,
description: EntityDescription,
user: PyTautulliApiUser | None = None,
) -> None:
"""Initialize the Tautulli entity."""
super().__init__(coordinator)
entry_id = coordinator.config_entry.entry_id
self._attr_unique_id = f"{entry_id}_{description.key}"
self.entity_description = description
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}"
self.user = user
self._attr_device_info = DeviceInfo(
configuration_url=coordinator.host_configuration.base_url,
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
identifiers={(DOMAIN, user.user_id if user else entry_id)},
manufacturer=DEFAULT_NAME,
name=user.username if user else DEFAULT_NAME,
)

View File

@ -1,6 +1,8 @@
"""Constants for the Tautulli integration."""
from logging import Logger, getLogger
ATTR_TOP_USER = "top_user"
CONF_MONITORED_USERS = "monitored_users"
DEFAULT_NAME = "Tautulli"
DEFAULT_PATH = ""

View File

@ -1,14 +1,23 @@
"""A platform which allows you to get information from Tautulli."""
from __future__ import annotations
from typing import Any
from collections.abc import Callable
from dataclasses import dataclass
from typing import cast
from pytautulli import (
PyTautulliApiActivity,
PyTautulliApiHomeStats,
PyTautulliApiSession,
PyTautulliApiUser,
)
import voluptuous as vol
from homeassistant.components.sensor import (
PLATFORM_SCHEMA,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
@ -20,14 +29,18 @@ from homeassistant.const import (
CONF_PORT,
CONF_SSL,
CONF_VERIFY_SSL,
DATA_KILOBITS,
PERCENTAGE,
)
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import EntityCategory, EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from . import TautulliEntity
from .const import (
ATTR_TOP_USER,
CONF_MONITORED_USERS,
DEFAULT_NAME,
DEFAULT_PATH,
@ -53,12 +66,188 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
}
)
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
def get_top_stats(
home_stats: PyTautulliApiHomeStats, activity: PyTautulliApiActivity, key: str
) -> str | None:
"""Get top statistics."""
value = None
for stat in home_stats:
if stat.rows and stat.stat_id == key:
value = stat.rows[0].title
elif stat.rows and stat.stat_id == "top_users" and key == ATTR_TOP_USER:
value = stat.rows[0].user
return value
@dataclass
class TautulliSensorEntityMixin:
"""Mixin for Tautulli sensor."""
value_fn: Callable[[PyTautulliApiHomeStats, PyTautulliApiActivity, str], StateType]
@dataclass
class TautulliSensorEntityDescription(
SensorEntityDescription, TautulliSensorEntityMixin
):
"""Describes a Tautulli sensor."""
SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = (
TautulliSensorEntityDescription(
icon="mdi:plex",
key="watching_count",
name="Tautulli",
native_unit_of_measurement="Watching",
value_fn=lambda home_stats, activity, _: cast(int, activity.stream_count),
),
TautulliSensorEntityDescription(
icon="mdi:plex",
key="stream_count_direct_play",
name="Direct Plays",
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement="Streams",
entity_registry_enabled_default=False,
value_fn=lambda home_stats, activity, _: cast(
int, activity.stream_count_direct_play
),
),
TautulliSensorEntityDescription(
icon="mdi:plex",
key="stream_count_direct_stream",
name="Direct Streams",
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement="Streams",
entity_registry_enabled_default=False,
value_fn=lambda home_stats, activity, _: cast(
int, activity.stream_count_direct_stream
),
),
TautulliSensorEntityDescription(
icon="mdi:plex",
key="stream_count_transcode",
name="Transcodes",
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement="Streams",
entity_registry_enabled_default=False,
value_fn=lambda home_stats, activity, _: cast(
int, activity.stream_count_transcode
),
),
TautulliSensorEntityDescription(
key="total_bandwidth",
name="Total Bandwidth",
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=DATA_KILOBITS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda home_stats, activity, _: cast(int, activity.total_bandwidth),
),
TautulliSensorEntityDescription(
key="lan_bandwidth",
name="LAN Bandwidth",
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=DATA_KILOBITS,
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda home_stats, activity, _: cast(int, activity.lan_bandwidth),
),
TautulliSensorEntityDescription(
key="wan_bandwidth",
name="WAN Bandwidth",
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=DATA_KILOBITS,
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda home_stats, activity, _: cast(int, activity.wan_bandwidth),
),
TautulliSensorEntityDescription(
icon="mdi:movie-open",
key="top_movies",
name="Top Movie",
entity_registry_enabled_default=False,
value_fn=get_top_stats,
),
TautulliSensorEntityDescription(
icon="mdi:television",
key="top_tv",
name="Top TV Show",
entity_registry_enabled_default=False,
value_fn=get_top_stats,
),
TautulliSensorEntityDescription(
icon="mdi:walk",
key=ATTR_TOP_USER,
name="Top User",
entity_registry_enabled_default=False,
value_fn=get_top_stats,
),
)
@dataclass
class TautulliSessionSensorEntityMixin:
"""Mixin for Tautulli session sensor."""
value_fn: Callable[[PyTautulliApiSession], StateType]
@dataclass
class TautulliSessionSensorEntityDescription(
SensorEntityDescription, TautulliSessionSensorEntityMixin
):
"""Describes a Tautulli session sensor."""
SESSION_SENSOR_TYPES: tuple[TautulliSessionSensorEntityDescription, ...] = (
TautulliSessionSensorEntityDescription(
icon="mdi:plex",
key="state",
name="State",
value_fn=lambda session: cast(str, session.state),
),
TautulliSessionSensorEntityDescription(
key="full_title",
name="Full Title",
entity_registry_enabled_default=False,
value_fn=lambda session: cast(str, session.full_title),
),
TautulliSessionSensorEntityDescription(
icon="mdi:progress-clock",
key="progress",
name="Progress",
native_unit_of_measurement=PERCENTAGE,
entity_registry_enabled_default=False,
value_fn=lambda session: cast(str, session.progress_percent),
),
TautulliSessionSensorEntityDescription(
key="stream_resolution",
name="Stream Resolution",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda session: cast(str, session.stream_video_resolution),
),
TautulliSessionSensorEntityDescription(
icon="mdi:plex",
key="transcode_decision",
name="Transcode Decision",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda session: cast(str, session.transcode_decision),
),
TautulliSessionSensorEntityDescription(
key="session_thumb",
name="session Thumbnail",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda session: cast(str, session.user_thumb),
),
TautulliSessionSensorEntityDescription(
key="video_resolution",
name="Video Resolution",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda session: cast(str, session.video_resolution),
),
)
@ -82,62 +271,64 @@ async def async_setup_entry(
) -> None:
"""Set up Tautulli sensor."""
coordinator: TautulliDataUpdateCoordinator = hass.data[DOMAIN]
async_add_entities(
entities: list[TautulliSensor | TautulliSessionSensor] = [
TautulliSensor(
coordinator,
description,
)
for description in SENSOR_TYPES
]
if coordinator.users:
entities.extend(
TautulliSessionSensor(
coordinator,
description,
user,
)
for description in SESSION_SENSOR_TYPES
for user in coordinator.users
if user.username != "Local"
)
async_add_entities(entities)
class TautulliSensor(TautulliEntity, SensorEntity):
"""Representation of a Tautulli sensor."""
entity_description: TautulliSensorEntityDescription
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
if not self.coordinator.activity:
return 0
return self.coordinator.activity.stream_count or 0
return self.entity_description.value_fn(
self.coordinator.home_stats,
self.coordinator.activity,
self.entity_description.key,
)
class TautulliSessionSensor(TautulliEntity, SensorEntity):
"""Representation of a Tautulli session sensor."""
entity_description: TautulliSessionSensorEntityDescription
def __init__(
self,
coordinator: TautulliDataUpdateCoordinator,
description: EntityDescription,
user: PyTautulliApiUser,
) -> None:
"""Initialize the Tautulli entity."""
super().__init__(coordinator, description, user)
entry_id = coordinator.config_entry.entry_id
self._attr_unique_id = f"{entry_id}_{user.user_id}_{description.key}"
self._attr_name = f"{user.username} {description.name}"
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return attributes for the sensor."""
if (
not self.coordinator.activity
or not self.coordinator.home_stats
or not self.coordinator.users
):
return None
_attributes = {
"stream_count": self.coordinator.activity.stream_count,
"stream_count_direct_play": self.coordinator.activity.stream_count_direct_play,
"stream_count_direct_stream": self.coordinator.activity.stream_count_direct_stream,
"stream_count_transcode": self.coordinator.activity.stream_count_transcode,
"total_bandwidth": self.coordinator.activity.total_bandwidth,
"lan_bandwidth": self.coordinator.activity.lan_bandwidth,
"wan_bandwidth": self.coordinator.activity.wan_bandwidth,
}
for stat in self.coordinator.home_stats:
if stat.stat_id == "top_movies":
_attributes["Top Movie"] = stat.rows[0].title if stat.rows else None
elif stat.stat_id == "top_tv":
_attributes["Top TV Show"] = stat.rows[0].title if stat.rows else None
elif stat.stat_id == "top_users":
_attributes["Top User"] = stat.rows[0].user if stat.rows else None
for user in self.coordinator.users:
if user.username == "Local":
continue
_attributes.setdefault(user.username, {})["Activity"] = None
def native_value(self) -> StateType:
"""Return the state of the sensor."""
if self.coordinator.activity:
for session in self.coordinator.activity.sessions:
if not _attributes.get(session.username) or "null" in session.state:
continue
_attributes[session.username]["Activity"] = session.state
return _attributes
if self.user and session.user_id == self.user.user_id:
return self.entity_description.value_fn(session)
return None