mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Move Tautulli attributes to their own sensors (#71712)
This commit is contained in:
parent
981249d330
commit
500105fa86
@ -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,
|
||||
)
|
||||
|
@ -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 = ""
|
||||
|
@ -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
|
||||
|
||||
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
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
if self.coordinator.activity:
|
||||
for session in self.coordinator.activity.sessions:
|
||||
if self.user and session.user_id == self.user.user_id:
|
||||
return self.entity_description.value_fn(session)
|
||||
return None
|
||||
|
Loading…
x
Reference in New Issue
Block a user