mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +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."""
|
"""The Tautulli integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pytautulli import PyTautulli, PyTautulliHostConfiguration
|
from pytautulli import PyTautulli, PyTautulliApiUser, PyTautulliHostConfiguration
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL, Platform
|
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL, Platform
|
||||||
@ -50,14 +50,18 @@ class TautulliEntity(CoordinatorEntity[TautulliDataUpdateCoordinator]):
|
|||||||
self,
|
self,
|
||||||
coordinator: TautulliDataUpdateCoordinator,
|
coordinator: TautulliDataUpdateCoordinator,
|
||||||
description: EntityDescription,
|
description: EntityDescription,
|
||||||
|
user: PyTautulliApiUser | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the Tautulli entity."""
|
"""Initialize the Tautulli entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
entry_id = coordinator.config_entry.entry_id
|
||||||
|
self._attr_unique_id = f"{entry_id}_{description.key}"
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}"
|
self.user = user
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
configuration_url=coordinator.host_configuration.base_url,
|
configuration_url=coordinator.host_configuration.base_url,
|
||||||
entry_type=DeviceEntryType.SERVICE,
|
entry_type=DeviceEntryType.SERVICE,
|
||||||
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
|
identifiers={(DOMAIN, user.user_id if user else entry_id)},
|
||||||
manufacturer=DEFAULT_NAME,
|
manufacturer=DEFAULT_NAME,
|
||||||
|
name=user.username if user else DEFAULT_NAME,
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
"""Constants for the Tautulli integration."""
|
"""Constants for the Tautulli integration."""
|
||||||
from logging import Logger, getLogger
|
from logging import Logger, getLogger
|
||||||
|
|
||||||
|
ATTR_TOP_USER = "top_user"
|
||||||
|
|
||||||
CONF_MONITORED_USERS = "monitored_users"
|
CONF_MONITORED_USERS = "monitored_users"
|
||||||
DEFAULT_NAME = "Tautulli"
|
DEFAULT_NAME = "Tautulli"
|
||||||
DEFAULT_PATH = ""
|
DEFAULT_PATH = ""
|
||||||
|
@ -1,14 +1,23 @@
|
|||||||
"""A platform which allows you to get information from Tautulli."""
|
"""A platform which allows you to get information from Tautulli."""
|
||||||
from __future__ import annotations
|
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
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -20,14 +29,18 @@ from homeassistant.const import (
|
|||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_SSL,
|
CONF_SSL,
|
||||||
CONF_VERIFY_SSL,
|
CONF_VERIFY_SSL,
|
||||||
|
DATA_KILOBITS,
|
||||||
|
PERCENTAGE,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.entity import EntityCategory, EntityDescription
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
||||||
|
|
||||||
from . import TautulliEntity
|
from . import TautulliEntity
|
||||||
from .const import (
|
from .const import (
|
||||||
|
ATTR_TOP_USER,
|
||||||
CONF_MONITORED_USERS,
|
CONF_MONITORED_USERS,
|
||||||
DEFAULT_NAME,
|
DEFAULT_NAME,
|
||||||
DEFAULT_PATH,
|
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",
|
icon="mdi:plex",
|
||||||
key="watching_count",
|
key="watching_count",
|
||||||
name="Tautulli",
|
name="Tautulli",
|
||||||
native_unit_of_measurement="Watching",
|
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:
|
) -> None:
|
||||||
"""Set up Tautulli sensor."""
|
"""Set up Tautulli sensor."""
|
||||||
coordinator: TautulliDataUpdateCoordinator = hass.data[DOMAIN]
|
coordinator: TautulliDataUpdateCoordinator = hass.data[DOMAIN]
|
||||||
async_add_entities(
|
entities: list[TautulliSensor | TautulliSessionSensor] = [
|
||||||
TautulliSensor(
|
TautulliSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
description,
|
description,
|
||||||
)
|
)
|
||||||
for description in SENSOR_TYPES
|
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):
|
class TautulliSensor(TautulliEntity, SensorEntity):
|
||||||
"""Representation of a Tautulli sensor."""
|
"""Representation of a Tautulli sensor."""
|
||||||
|
|
||||||
|
entity_description: TautulliSensorEntityDescription
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
if not self.coordinator.activity:
|
return self.entity_description.value_fn(
|
||||||
return 0
|
self.coordinator.home_stats,
|
||||||
return self.coordinator.activity.stream_count or 0
|
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
|
@property
|
||||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
def native_value(self) -> StateType:
|
||||||
"""Return attributes for the sensor."""
|
"""Return the state of the sensor."""
|
||||||
if (
|
if self.coordinator.activity:
|
||||||
not self.coordinator.activity
|
for session in self.coordinator.activity.sessions:
|
||||||
or not self.coordinator.home_stats
|
if self.user and session.user_id == self.user.user_id:
|
||||||
or not self.coordinator.users
|
return self.entity_description.value_fn(session)
|
||||||
):
|
return None
|
||||||
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
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user