mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add status sensors to paperless (#145591)
* Add first status sensor and coordinator * New snapshot * Add comment * Add test for forbidden status endpoint * Changed comment * Fixed translation * Minor changes and code optimization * Add common translation; minor tweaks * Moved translation from common to integration
This commit is contained in:
parent
b7ce0f63a9
commit
c14d17f88c
@ -1,9 +1,30 @@
|
||||
"""The Paperless-ngx integration."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from pypaperless import Paperless
|
||||
from pypaperless.exceptions import (
|
||||
InitializationError,
|
||||
PaperlessConnectionError,
|
||||
PaperlessForbiddenError,
|
||||
PaperlessInactiveOrDeletedError,
|
||||
PaperlessInvalidTokenError,
|
||||
)
|
||||
|
||||
from .coordinator import PaperlessConfigEntry, PaperlessCoordinator
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryError,
|
||||
ConfigEntryNotReady,
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .coordinator import (
|
||||
PaperlessConfigEntry,
|
||||
PaperlessData,
|
||||
PaperlessStatisticCoordinator,
|
||||
PaperlessStatusCoordinator,
|
||||
)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
@ -11,10 +32,28 @@ PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PaperlessConfigEntry) -> bool:
|
||||
"""Set up Paperless-ngx from a config entry."""
|
||||
|
||||
coordinator = PaperlessCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
api = await _get_paperless_api(hass, entry)
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
statistics_coordinator = PaperlessStatisticCoordinator(hass, entry, api)
|
||||
status_coordinator = PaperlessStatusCoordinator(hass, entry, api)
|
||||
|
||||
await statistics_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
try:
|
||||
await status_coordinator.async_config_entry_first_refresh()
|
||||
except ConfigEntryNotReady as err:
|
||||
# Catch the error so the integration doesn't fail just because status coordinator fails.
|
||||
LOGGER.warning("Could not initialize status coordinator: %s", err)
|
||||
|
||||
entry.runtime_data = PaperlessData(
|
||||
status=status_coordinator,
|
||||
statistics=statistics_coordinator,
|
||||
)
|
||||
|
||||
entry.runtime_data = PaperlessData(
|
||||
status=status_coordinator,
|
||||
statistics=statistics_coordinator,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@ -24,3 +63,47 @@ async def async_setup_entry(hass: HomeAssistant, entry: PaperlessConfigEntry) ->
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: PaperlessConfigEntry) -> bool:
|
||||
"""Unload paperless-ngx config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def _get_paperless_api(
|
||||
hass: HomeAssistant,
|
||||
entry: PaperlessConfigEntry,
|
||||
) -> Paperless:
|
||||
"""Create and initialize paperless-ngx API."""
|
||||
|
||||
api = Paperless(
|
||||
entry.data[CONF_URL],
|
||||
entry.data[CONF_API_KEY],
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
try:
|
||||
await api.initialize()
|
||||
await api.statistics() # test permissions on api
|
||||
except PaperlessConnectionError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
except PaperlessInvalidTokenError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_api_key",
|
||||
) from err
|
||||
except PaperlessInactiveOrDeletedError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="user_inactive_or_deleted",
|
||||
) from err
|
||||
except PaperlessForbiddenError as err:
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="forbidden",
|
||||
) from err
|
||||
except InitializationError as err:
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
else:
|
||||
return api
|
||||
|
@ -2,38 +2,45 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import TypeVar
|
||||
|
||||
from pypaperless import Paperless
|
||||
from pypaperless.exceptions import (
|
||||
InitializationError,
|
||||
PaperlessConnectionError,
|
||||
PaperlessForbiddenError,
|
||||
PaperlessInactiveOrDeletedError,
|
||||
PaperlessInvalidTokenError,
|
||||
)
|
||||
from pypaperless.models import Statistic
|
||||
from pypaperless.models import Statistic, Status
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryError,
|
||||
ConfigEntryNotReady,
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
type PaperlessConfigEntry = ConfigEntry[PaperlessCoordinator]
|
||||
type PaperlessConfigEntry = ConfigEntry[PaperlessData]
|
||||
|
||||
UPDATE_INTERVAL = 120
|
||||
TData = TypeVar("TData")
|
||||
|
||||
UPDATE_INTERVAL_STATISTICS = timedelta(seconds=120)
|
||||
UPDATE_INTERVAL_STATUS = timedelta(seconds=300)
|
||||
|
||||
|
||||
class PaperlessCoordinator(DataUpdateCoordinator[Statistic]):
|
||||
"""Coordinator to manage Paperless-ngx statistic updates."""
|
||||
@dataclass
|
||||
class PaperlessData:
|
||||
"""Data for the Paperless-ngx integration."""
|
||||
|
||||
statistics: PaperlessStatisticCoordinator
|
||||
status: PaperlessStatusCoordinator
|
||||
|
||||
|
||||
class PaperlessCoordinator(DataUpdateCoordinator[TData]):
|
||||
"""Coordinator to manage fetching Paperless-ngx API."""
|
||||
|
||||
config_entry: PaperlessConfigEntry
|
||||
|
||||
@ -41,28 +48,27 @@ class PaperlessCoordinator(DataUpdateCoordinator[Statistic]):
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: PaperlessConfigEntry,
|
||||
api: Paperless,
|
||||
name: str,
|
||||
update_interval: timedelta,
|
||||
) -> None:
|
||||
"""Initialize my coordinator."""
|
||||
"""Initialize Paperless-ngx statistics coordinator."""
|
||||
self.api = api
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
LOGGER,
|
||||
config_entry=entry,
|
||||
name="Paperless-ngx Coordinator",
|
||||
update_interval=timedelta(seconds=UPDATE_INTERVAL),
|
||||
name=name,
|
||||
update_interval=update_interval,
|
||||
)
|
||||
|
||||
self.api = Paperless(
|
||||
entry.data[CONF_URL],
|
||||
entry.data[CONF_API_KEY],
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
async def _async_update_data(self) -> TData:
|
||||
"""Update data via internal method."""
|
||||
try:
|
||||
await self.api.initialize()
|
||||
await self.api.statistics() # test permissions on api
|
||||
return await self._async_update_data_internal()
|
||||
except PaperlessConnectionError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
@ -77,37 +83,57 @@ class PaperlessCoordinator(DataUpdateCoordinator[Statistic]):
|
||||
translation_key="user_inactive_or_deleted",
|
||||
) from err
|
||||
except PaperlessForbiddenError as err:
|
||||
raise ConfigEntryError(
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="forbidden",
|
||||
) from err
|
||||
except InitializationError as err:
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
|
||||
async def _async_update_data(self) -> Statistic:
|
||||
"""Fetch data from API endpoint."""
|
||||
try:
|
||||
@abstractmethod
|
||||
async def _async_update_data_internal(self) -> TData:
|
||||
"""Update data via paperless-ngx API."""
|
||||
|
||||
|
||||
class PaperlessStatisticCoordinator(PaperlessCoordinator[Statistic]):
|
||||
"""Coordinator to manage Paperless-ngx statistic updates."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: PaperlessConfigEntry,
|
||||
api: Paperless,
|
||||
) -> None:
|
||||
"""Initialize Paperless-ngx status coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
entry,
|
||||
api,
|
||||
name="Statistics Coordinator",
|
||||
update_interval=UPDATE_INTERVAL_STATISTICS,
|
||||
)
|
||||
|
||||
async def _async_update_data_internal(self) -> Statistic:
|
||||
"""Fetch statistics data from API endpoint."""
|
||||
return await self.api.statistics()
|
||||
except PaperlessConnectionError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
except PaperlessForbiddenError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="forbidden",
|
||||
) from err
|
||||
except PaperlessInvalidTokenError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_api_key",
|
||||
) from err
|
||||
except PaperlessInactiveOrDeletedError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="user_inactive_or_deleted",
|
||||
) from err
|
||||
|
||||
|
||||
class PaperlessStatusCoordinator(PaperlessCoordinator[Status]):
|
||||
"""Coordinator to manage Paperless-ngx status updates."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: PaperlessConfigEntry,
|
||||
api: Paperless,
|
||||
) -> None:
|
||||
"""Initialize Paperless-ngx status coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
entry,
|
||||
api,
|
||||
name="Status Coordinator",
|
||||
update_interval=UPDATE_INTERVAL_STATUS,
|
||||
)
|
||||
|
||||
async def _async_update_data_internal(self) -> Status:
|
||||
"""Fetch status data from API endpoint."""
|
||||
return await self.api.status()
|
||||
|
@ -15,4 +15,9 @@ async def async_get_config_entry_diagnostics(
|
||||
entry: PaperlessConfigEntry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
return {"data": asdict(entry.runtime_data.data)}
|
||||
return {
|
||||
"data": {
|
||||
"statistics": asdict(entry.runtime_data.statistics.data),
|
||||
"status": asdict(entry.runtime_data.status.data),
|
||||
},
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from homeassistant.components.sensor import EntityDescription
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@ -9,15 +11,17 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from .const import DOMAIN
|
||||
from .coordinator import PaperlessCoordinator
|
||||
|
||||
TCoordinator = TypeVar("TCoordinator", bound=PaperlessCoordinator)
|
||||
|
||||
class PaperlessEntity(CoordinatorEntity[PaperlessCoordinator]):
|
||||
|
||||
class PaperlessEntity(CoordinatorEntity[TCoordinator], Generic[TCoordinator]):
|
||||
"""Defines a base Paperless-ngx entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PaperlessCoordinator,
|
||||
coordinator: TCoordinator,
|
||||
description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the Paperless-ngx entity."""
|
||||
|
@ -16,8 +16,65 @@
|
||||
"correspondent_count": {
|
||||
"default": "mdi:account-group"
|
||||
},
|
||||
"document_type_count": {
|
||||
"default": "mdi:format-list-bulleted-type"
|
||||
"storage_total": {
|
||||
"default": "mdi:harddisk"
|
||||
},
|
||||
"storage_available": {
|
||||
"default": "mdi:harddisk"
|
||||
},
|
||||
"database_status": {
|
||||
"default": "mdi:check-circle",
|
||||
"state": {
|
||||
"ok": "mdi:check-circle",
|
||||
"warning": "mdi:alert",
|
||||
"error": "mdi:alert-circle",
|
||||
"unknown": "mdi:help-circle"
|
||||
}
|
||||
},
|
||||
"index_status": {
|
||||
"default": "mdi:check-circle",
|
||||
"state": {
|
||||
"ok": "mdi:check-circle",
|
||||
"warning": "mdi:alert",
|
||||
"error": "mdi:alert-circle",
|
||||
"unknown": "mdi:help-circle"
|
||||
}
|
||||
},
|
||||
"classifier_status": {
|
||||
"default": "mdi:check-circle",
|
||||
"state": {
|
||||
"ok": "mdi:check-circle",
|
||||
"warning": "mdi:alert",
|
||||
"error": "mdi:alert-circle",
|
||||
"unknown": "mdi:help-circle"
|
||||
}
|
||||
},
|
||||
"celery_status": {
|
||||
"default": "mdi:check-circle",
|
||||
"state": {
|
||||
"ok": "mdi:check-circle",
|
||||
"warning": "mdi:alert",
|
||||
"error": "mdi:alert-circle",
|
||||
"unknown": "mdi:help-circle"
|
||||
}
|
||||
},
|
||||
"redis_status": {
|
||||
"default": "mdi:check-circle",
|
||||
"state": {
|
||||
"ok": "mdi:check-circle",
|
||||
"warning": "mdi:alert",
|
||||
"error": "mdi:alert-circle",
|
||||
"unknown": "mdi:help-circle"
|
||||
}
|
||||
},
|
||||
"sanity_check_status": {
|
||||
"default": "mdi:check-circle",
|
||||
"state": {
|
||||
"ok": "mdi:check-circle",
|
||||
"warning": "mdi:alert",
|
||||
"error": "mdi:alert-circle",
|
||||
"unknown": "mdi:help-circle"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,62 +4,73 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic
|
||||
|
||||
from pypaperless.models import Statistic
|
||||
from pypaperless.models import Statistic, Status
|
||||
from pypaperless.models.common import StatusType
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import EntityCategory, UnitOfInformation
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util.unit_conversion import InformationConverter
|
||||
|
||||
from .coordinator import PaperlessConfigEntry
|
||||
from .entity import PaperlessEntity
|
||||
from .coordinator import (
|
||||
PaperlessConfigEntry,
|
||||
PaperlessStatisticCoordinator,
|
||||
PaperlessStatusCoordinator,
|
||||
TData,
|
||||
)
|
||||
from .entity import PaperlessEntity, TCoordinator
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class PaperlessEntityDescription(SensorEntityDescription):
|
||||
class PaperlessEntityDescription(SensorEntityDescription, Generic[TData]):
|
||||
"""Describes Paperless-ngx sensor entity."""
|
||||
|
||||
value_fn: Callable[[Statistic], int | None]
|
||||
value_fn: Callable[[TData], StateType]
|
||||
|
||||
|
||||
SENSOR_DESCRIPTIONS: tuple[PaperlessEntityDescription, ...] = (
|
||||
PaperlessEntityDescription(
|
||||
SENSOR_STATISTICS: tuple[PaperlessEntityDescription, ...] = (
|
||||
PaperlessEntityDescription[Statistic](
|
||||
key="documents_total",
|
||||
translation_key="documents_total",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.documents_total,
|
||||
),
|
||||
PaperlessEntityDescription(
|
||||
PaperlessEntityDescription[Statistic](
|
||||
key="documents_inbox",
|
||||
translation_key="documents_inbox",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.documents_inbox,
|
||||
),
|
||||
PaperlessEntityDescription(
|
||||
PaperlessEntityDescription[Statistic](
|
||||
key="characters_count",
|
||||
translation_key="characters_count",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.character_count,
|
||||
),
|
||||
PaperlessEntityDescription(
|
||||
PaperlessEntityDescription[Statistic](
|
||||
key="tag_count",
|
||||
translation_key="tag_count",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.tag_count,
|
||||
),
|
||||
PaperlessEntityDescription(
|
||||
PaperlessEntityDescription[Statistic](
|
||||
key="correspondent_count",
|
||||
translation_key="correspondent_count",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.correspondent_count,
|
||||
),
|
||||
PaperlessEntityDescription(
|
||||
PaperlessEntityDescription[Statistic](
|
||||
key="document_type_count",
|
||||
translation_key="document_type_count",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@ -67,6 +78,157 @@ SENSOR_DESCRIPTIONS: tuple[PaperlessEntityDescription, ...] = (
|
||||
),
|
||||
)
|
||||
|
||||
SENSOR_STATUS: tuple[PaperlessEntityDescription, ...] = (
|
||||
PaperlessEntityDescription[Status](
|
||||
key="storage_total",
|
||||
translation_key="storage_total",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=(
|
||||
lambda data: round(
|
||||
InformationConverter().convert(
|
||||
data.storage.total,
|
||||
UnitOfInformation.BYTES,
|
||||
UnitOfInformation.GIGABYTES,
|
||||
),
|
||||
2,
|
||||
)
|
||||
if data.storage is not None and data.storage.total is not None
|
||||
else None
|
||||
),
|
||||
),
|
||||
PaperlessEntityDescription[Status](
|
||||
key="storage_available",
|
||||
translation_key="storage_available",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=(
|
||||
lambda data: round(
|
||||
InformationConverter().convert(
|
||||
data.storage.available,
|
||||
UnitOfInformation.BYTES,
|
||||
UnitOfInformation.GIGABYTES,
|
||||
),
|
||||
2,
|
||||
)
|
||||
if data.storage is not None and data.storage.available is not None
|
||||
else None
|
||||
),
|
||||
),
|
||||
PaperlessEntityDescription[Status](
|
||||
key="database_status",
|
||||
translation_key="database_status",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=[
|
||||
item.value.lower() for item in StatusType if item != StatusType.UNKNOWN
|
||||
],
|
||||
value_fn=(
|
||||
lambda data: data.database.status.value.lower()
|
||||
if (
|
||||
data.database is not None
|
||||
and data.database.status is not None
|
||||
and data.database.status != StatusType.UNKNOWN
|
||||
)
|
||||
else None
|
||||
),
|
||||
),
|
||||
PaperlessEntityDescription[Status](
|
||||
key="index_status",
|
||||
translation_key="index_status",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=[
|
||||
item.value.lower() for item in StatusType if item != StatusType.UNKNOWN
|
||||
],
|
||||
value_fn=(
|
||||
lambda data: data.tasks.index_status.value.lower()
|
||||
if (
|
||||
data.tasks is not None
|
||||
and data.tasks.index_status is not None
|
||||
and data.tasks.index_status != StatusType.UNKNOWN
|
||||
)
|
||||
else None
|
||||
),
|
||||
),
|
||||
PaperlessEntityDescription[Status](
|
||||
key="classifier_status",
|
||||
translation_key="classifier_status",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=[
|
||||
item.value.lower() for item in StatusType if item != StatusType.UNKNOWN
|
||||
],
|
||||
value_fn=(
|
||||
lambda data: data.tasks.classifier_status.value.lower()
|
||||
if (
|
||||
data.tasks is not None
|
||||
and data.tasks.classifier_status is not None
|
||||
and data.tasks.classifier_status != StatusType.UNKNOWN
|
||||
)
|
||||
else None
|
||||
),
|
||||
),
|
||||
PaperlessEntityDescription[Status](
|
||||
key="celery_status",
|
||||
translation_key="celery_status",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=[
|
||||
item.value.lower() for item in StatusType if item != StatusType.UNKNOWN
|
||||
],
|
||||
value_fn=(
|
||||
lambda data: data.tasks.celery_status.value.lower()
|
||||
if (
|
||||
data.tasks is not None
|
||||
and data.tasks.celery_status is not None
|
||||
and data.tasks.celery_status != StatusType.UNKNOWN
|
||||
)
|
||||
else None
|
||||
),
|
||||
),
|
||||
PaperlessEntityDescription[Status](
|
||||
key="redis_status",
|
||||
translation_key="redis_status",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=[
|
||||
item.value.lower() for item in StatusType if item != StatusType.UNKNOWN
|
||||
],
|
||||
value_fn=(
|
||||
lambda data: data.tasks.redis_status.value.lower()
|
||||
if (
|
||||
data.tasks is not None
|
||||
and data.tasks.redis_status is not None
|
||||
and data.tasks.redis_status != StatusType.UNKNOWN
|
||||
)
|
||||
else None
|
||||
),
|
||||
),
|
||||
PaperlessEntityDescription[Status](
|
||||
key="sanity_check_status",
|
||||
translation_key="sanity_check_status",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=[
|
||||
item.value.lower() for item in StatusType if item != StatusType.UNKNOWN
|
||||
],
|
||||
value_fn=(
|
||||
lambda data: data.tasks.sanity_check_status.value.lower()
|
||||
if (
|
||||
data.tasks is not None
|
||||
and data.tasks.sanity_check_status is not None
|
||||
and data.tasks.sanity_check_status != StatusType.UNKNOWN
|
||||
)
|
||||
else None
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@ -74,21 +236,34 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Paperless-ngx sensors."""
|
||||
async_add_entities(
|
||||
PaperlessSensor(
|
||||
coordinator=entry.runtime_data,
|
||||
description=sensor_description,
|
||||
|
||||
entities: list[PaperlessSensor] = []
|
||||
|
||||
entities += [
|
||||
PaperlessSensor[PaperlessStatisticCoordinator](
|
||||
coordinator=entry.runtime_data.statistics,
|
||||
description=description,
|
||||
)
|
||||
for sensor_description in SENSOR_DESCRIPTIONS
|
||||
for description in SENSOR_STATISTICS
|
||||
]
|
||||
|
||||
entities += [
|
||||
PaperlessSensor[PaperlessStatusCoordinator](
|
||||
coordinator=entry.runtime_data.status,
|
||||
description=description,
|
||||
)
|
||||
for description in SENSOR_STATUS
|
||||
]
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class PaperlessSensor(PaperlessEntity, SensorEntity):
|
||||
class PaperlessSensor(PaperlessEntity[TCoordinator], SensorEntity):
|
||||
"""Defines a Paperless-ngx sensor entity."""
|
||||
|
||||
entity_description: PaperlessEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | None:
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the current value of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
@ -71,6 +71,60 @@
|
||||
"document_type_count": {
|
||||
"name": "Document types",
|
||||
"unit_of_measurement": "document types"
|
||||
},
|
||||
"storage_total": {
|
||||
"name": "Total storage"
|
||||
},
|
||||
"storage_available": {
|
||||
"name": "Available storage"
|
||||
},
|
||||
"database_status": {
|
||||
"name": "Status database",
|
||||
"state": {
|
||||
"ok": "OK",
|
||||
"warning": "Warning",
|
||||
"error": "[%key:common::state::error%]"
|
||||
}
|
||||
},
|
||||
"index_status": {
|
||||
"name": "Status index",
|
||||
"state": {
|
||||
"ok": "[%key:component::paperless_ngx::entity::sensor::database_status::state::ok%]",
|
||||
"warning": "[%key:component::paperless_ngx::entity::sensor::database_status::state::warning%]",
|
||||
"error": "[%key:common::state::error%]"
|
||||
}
|
||||
},
|
||||
"classifier_status": {
|
||||
"name": "Status classifier",
|
||||
"state": {
|
||||
"ok": "[%key:component::paperless_ngx::entity::sensor::database_status::state::ok%]",
|
||||
"warning": "[%key:component::paperless_ngx::entity::sensor::database_status::state::warning%]",
|
||||
"error": "[%key:common::state::error%]"
|
||||
}
|
||||
},
|
||||
"celery_status": {
|
||||
"name": "Status celery",
|
||||
"state": {
|
||||
"ok": "[%key:component::paperless_ngx::entity::sensor::database_status::state::ok%]",
|
||||
"warning": "[%key:component::paperless_ngx::entity::sensor::database_status::state::warning%]",
|
||||
"error": "[%key:common::state::error%]"
|
||||
}
|
||||
},
|
||||
"redis_status": {
|
||||
"name": "Status redis",
|
||||
"state": {
|
||||
"ok": "[%key:component::paperless_ngx::entity::sensor::database_status::state::ok%]",
|
||||
"warning": "[%key:component::paperless_ngx::entity::sensor::database_status::state::warning%]",
|
||||
"error": "[%key:common::state::error%]"
|
||||
}
|
||||
},
|
||||
"sanity_check_status": {
|
||||
"name": "Status sanity",
|
||||
"state": {
|
||||
"ok": "[%key:component::paperless_ngx::entity::sensor::database_status::state::ok%]",
|
||||
"warning": "[%key:component::paperless_ngx::entity::sensor::database_status::state::warning%]",
|
||||
"error": "[%key:common::state::error%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -4,7 +4,7 @@ from collections.abc import Generator
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from pypaperless.models import Statistic
|
||||
from pypaperless.models import Statistic, Status
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.paperless_ngx.const import DOMAIN
|
||||
@ -16,6 +16,12 @@ from .const import USER_INPUT_ONE
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_status_data() -> Generator[MagicMock]:
|
||||
"""Return test status data."""
|
||||
return json.loads(load_fixture("test_data_status.json", DOMAIN))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_statistic_data() -> Generator[MagicMock]:
|
||||
"""Return test statistic data."""
|
||||
@ -29,7 +35,9 @@ def mock_statistic_data_update() -> Generator[MagicMock]:
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_paperless(mock_statistic_data: MagicMock) -> Generator[AsyncMock]:
|
||||
def mock_paperless(
|
||||
mock_statistic_data: MagicMock, mock_status_data: MagicMock
|
||||
) -> Generator[AsyncMock]:
|
||||
"""Mock the pypaperless.Paperless client."""
|
||||
with (
|
||||
patch(
|
||||
@ -40,6 +48,10 @@ def mock_paperless(mock_statistic_data: MagicMock) -> Generator[AsyncMock]:
|
||||
"homeassistant.components.paperless_ngx.config_flow.Paperless",
|
||||
new=paperless_mock,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.paperless_ngx.Paperless",
|
||||
new=paperless_mock,
|
||||
),
|
||||
):
|
||||
paperless = paperless_mock.return_value
|
||||
|
||||
@ -51,6 +63,11 @@ def mock_paperless(mock_statistic_data: MagicMock) -> Generator[AsyncMock]:
|
||||
paperless, data=mock_statistic_data, fetched=True
|
||||
)
|
||||
)
|
||||
paperless.status = AsyncMock(
|
||||
return_value=Status.create_with_data(
|
||||
paperless, data=mock_status_data, fetched=True
|
||||
)
|
||||
)
|
||||
|
||||
yield paperless
|
||||
|
||||
|
@ -0,0 +1,36 @@
|
||||
{
|
||||
"pngx_version": "2.15.3",
|
||||
"server_os": "Linux-6.6.74-haos-raspi-aarch64-with-glibc2.36",
|
||||
"install_type": "docker",
|
||||
"storage": {
|
||||
"total": 62101651456,
|
||||
"available": 25376927744
|
||||
},
|
||||
"database": {
|
||||
"type": "sqlite",
|
||||
"url": "/config/data/db.sqlite3",
|
||||
"status": "OK",
|
||||
"error": null,
|
||||
"migration_status": {
|
||||
"latest_migration": "paperless_mail.0029_mailrule_pdf_layout",
|
||||
"unapplied_migrations": []
|
||||
}
|
||||
},
|
||||
"tasks": {
|
||||
"redis_url": "redis://localhost:6379",
|
||||
"redis_status": "OK",
|
||||
"redis_error": null,
|
||||
"celery_status": "OK",
|
||||
"celery_url": "celery@ca5234a0-paperless-ngx",
|
||||
"celery_error": null,
|
||||
"index_status": "OK",
|
||||
"index_last_modified": "2025-05-25T00:00:27.053090+02:00",
|
||||
"index_error": null,
|
||||
"classifier_status": "OK",
|
||||
"classifier_last_trained": "2025-05-25T15:05:15.824671Z",
|
||||
"classifier_error": null,
|
||||
"sanity_check_status": "OK",
|
||||
"sanity_check_last_run": "2025-05-24T22:30:21.005536Z",
|
||||
"sanity_check_error": null
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
# name: test_config_entry_diagnostics
|
||||
dict({
|
||||
'data': dict({
|
||||
'statistics': dict({
|
||||
'character_count': 99999,
|
||||
'correspondent_count': 99,
|
||||
'current_asn': 99,
|
||||
@ -25,5 +26,61 @@
|
||||
'storage_path_count': 9,
|
||||
'tag_count': 99,
|
||||
}),
|
||||
'status': dict({
|
||||
'database': dict({
|
||||
'error': None,
|
||||
'migration_status': dict({
|
||||
'latest_migration': 'paperless_mail.0029_mailrule_pdf_layout',
|
||||
'unapplied_migrations': list([
|
||||
]),
|
||||
}),
|
||||
'status': dict({
|
||||
'__type': "<enum 'StatusType'>",
|
||||
'repr': "<StatusType.OK: 'OK'>",
|
||||
}),
|
||||
'type': 'sqlite',
|
||||
'url': '/config/data/db.sqlite3',
|
||||
}),
|
||||
'install_type': 'docker',
|
||||
'pngx_version': '2.15.3',
|
||||
'server_os': 'Linux-6.6.74-haos-raspi-aarch64-with-glibc2.36',
|
||||
'storage': dict({
|
||||
'available': 25376927744,
|
||||
'total': 62101651456,
|
||||
}),
|
||||
'tasks': dict({
|
||||
'celery_error': None,
|
||||
'celery_status': dict({
|
||||
'__type': "<enum 'StatusType'>",
|
||||
'repr': "<StatusType.OK: 'OK'>",
|
||||
}),
|
||||
'celery_url': 'celery@ca5234a0-paperless-ngx',
|
||||
'classifier_error': None,
|
||||
'classifier_last_trained': '2025-05-25T15:05:15.824671+00:00',
|
||||
'classifier_status': dict({
|
||||
'__type': "<enum 'StatusType'>",
|
||||
'repr': "<StatusType.OK: 'OK'>",
|
||||
}),
|
||||
'index_error': None,
|
||||
'index_last_modified': '2025-05-25T00:00:27.053090+02:00',
|
||||
'index_status': dict({
|
||||
'__type': "<enum 'StatusType'>",
|
||||
'repr': "<StatusType.OK: 'OK'>",
|
||||
}),
|
||||
'redis_error': None,
|
||||
'redis_status': dict({
|
||||
'__type': "<enum 'StatusType'>",
|
||||
'repr': "<StatusType.OK: 'OK'>",
|
||||
}),
|
||||
'redis_url': 'redis://localhost:6379',
|
||||
'sanity_check_error': None,
|
||||
'sanity_check_last_run': '2025-05-24T22:30:21.005536+00:00',
|
||||
'sanity_check_status': dict({
|
||||
'__type': "<enum 'StatusType'>",
|
||||
'repr': "<StatusType.OK: 'OK'>",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
|
@ -1,4 +1,56 @@
|
||||
# serializer version: 1
|
||||
# name: test_sensor_platform[sensor.paperless_ngx_available_storage-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.paperless_ngx_available_storage',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DATA_SIZE: 'data_size'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Available storage',
|
||||
'platform': 'paperless_ngx',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'storage_available',
|
||||
'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_storage_available',
|
||||
'unit_of_measurement': <UnitOfInformation.GIGABYTES: 'GB'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_platform[sensor.paperless_ngx_available_storage-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'data_size',
|
||||
'friendly_name': 'Paperless-ngx Available storage',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfInformation.GIGABYTES: 'GB'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.paperless_ngx_available_storage',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '25.38',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_platform[sensor.paperless_ngx_correspondents-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@ -152,6 +204,360 @@
|
||||
'state': '9',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_platform[sensor.paperless_ngx_status_celery-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'ok',
|
||||
'error',
|
||||
'warning',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.paperless_ngx_status_celery',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Status celery',
|
||||
'platform': 'paperless_ngx',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'celery_status',
|
||||
'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_celery_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_platform[sensor.paperless_ngx_status_celery-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Paperless-ngx Status celery',
|
||||
'options': list([
|
||||
'ok',
|
||||
'error',
|
||||
'warning',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.paperless_ngx_status_celery',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'ok',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_platform[sensor.paperless_ngx_status_classifier-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'ok',
|
||||
'error',
|
||||
'warning',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.paperless_ngx_status_classifier',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Status classifier',
|
||||
'platform': 'paperless_ngx',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'classifier_status',
|
||||
'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_classifier_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_platform[sensor.paperless_ngx_status_classifier-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Paperless-ngx Status classifier',
|
||||
'options': list([
|
||||
'ok',
|
||||
'error',
|
||||
'warning',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.paperless_ngx_status_classifier',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'ok',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_platform[sensor.paperless_ngx_status_database-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'ok',
|
||||
'error',
|
||||
'warning',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.paperless_ngx_status_database',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Status database',
|
||||
'platform': 'paperless_ngx',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'database_status',
|
||||
'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_database_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_platform[sensor.paperless_ngx_status_database-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Paperless-ngx Status database',
|
||||
'options': list([
|
||||
'ok',
|
||||
'error',
|
||||
'warning',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.paperless_ngx_status_database',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'ok',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_platform[sensor.paperless_ngx_status_index-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'ok',
|
||||
'error',
|
||||
'warning',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.paperless_ngx_status_index',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Status index',
|
||||
'platform': 'paperless_ngx',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'index_status',
|
||||
'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_index_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_platform[sensor.paperless_ngx_status_index-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Paperless-ngx Status index',
|
||||
'options': list([
|
||||
'ok',
|
||||
'error',
|
||||
'warning',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.paperless_ngx_status_index',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'ok',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_platform[sensor.paperless_ngx_status_redis-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'ok',
|
||||
'error',
|
||||
'warning',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.paperless_ngx_status_redis',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Status redis',
|
||||
'platform': 'paperless_ngx',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'redis_status',
|
||||
'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_redis_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_platform[sensor.paperless_ngx_status_redis-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Paperless-ngx Status redis',
|
||||
'options': list([
|
||||
'ok',
|
||||
'error',
|
||||
'warning',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.paperless_ngx_status_redis',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'ok',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_platform[sensor.paperless_ngx_status_sanity-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'ok',
|
||||
'error',
|
||||
'warning',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.paperless_ngx_status_sanity',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Status sanity',
|
||||
'platform': 'paperless_ngx',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'sanity_check_status',
|
||||
'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_sanity_check_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_platform[sensor.paperless_ngx_status_sanity-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Paperless-ngx Status sanity',
|
||||
'options': list([
|
||||
'ok',
|
||||
'error',
|
||||
'warning',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.paperless_ngx_status_sanity',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'ok',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_platform[sensor.paperless_ngx_tags-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@ -305,3 +711,55 @@
|
||||
'state': '999',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_platform[sensor.paperless_ngx_total_storage-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.paperless_ngx_total_storage',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DATA_SIZE: 'data_size'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Total storage',
|
||||
'platform': 'paperless_ngx',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'storage_total',
|
||||
'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_storage_total',
|
||||
'unit_of_measurement': <UnitOfInformation.GIGABYTES: 'GB'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_platform[sensor.paperless_ngx_total_storage-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'data_size',
|
||||
'friendly_name': 'Paperless-ngx Total storage',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfInformation.GIGABYTES: 'GB'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.paperless_ngx_total_storage',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '62.1',
|
||||
})
|
||||
# ---
|
||||
|
@ -34,10 +34,28 @@ async def test_load_unload_config_entry(
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_load_config_status_forbidden(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_paperless: AsyncMock,
|
||||
) -> None:
|
||||
"""Test loading and unloading the integration."""
|
||||
mock_paperless.status.side_effect = PaperlessForbiddenError
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "expected_state", "expected_error_key"),
|
||||
[
|
||||
(PaperlessConnectionError(), ConfigEntryState.SETUP_RETRY, None),
|
||||
(PaperlessConnectionError(), ConfigEntryState.SETUP_RETRY, "cannot_connect"),
|
||||
(PaperlessInvalidTokenError(), ConfigEntryState.SETUP_ERROR, "invalid_api_key"),
|
||||
(
|
||||
PaperlessInactiveOrDeletedError(),
|
||||
|
@ -1,7 +1,5 @@
|
||||
"""Tests for Paperless-ngx sensor platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pypaperless.exceptions import (
|
||||
PaperlessConnectionError,
|
||||
@ -12,7 +10,9 @@ from pypaperless.exceptions import (
|
||||
from pypaperless.models import Statistic
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.paperless_ngx.coordinator import UPDATE_INTERVAL
|
||||
from homeassistant.components.paperless_ngx.coordinator import (
|
||||
UPDATE_INTERVAL_STATISTICS,
|
||||
)
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@ -61,7 +61,7 @@ async def test_statistic_sensor_state(
|
||||
)
|
||||
)
|
||||
|
||||
freezer.tick(timedelta(seconds=UPDATE_INTERVAL))
|
||||
freezer.tick(UPDATE_INTERVAL_STATISTICS)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@ -91,7 +91,7 @@ async def test__statistic_sensor_state_on_error(
|
||||
# simulate error
|
||||
mock_paperless.statistics.side_effect = error_cls
|
||||
|
||||
freezer.tick(timedelta(seconds=UPDATE_INTERVAL))
|
||||
freezer.tick(UPDATE_INTERVAL_STATISTICS)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@ -105,7 +105,7 @@ async def test__statistic_sensor_state_on_error(
|
||||
)
|
||||
)
|
||||
|
||||
freezer.tick(timedelta(seconds=UPDATE_INTERVAL))
|
||||
freezer.tick(UPDATE_INTERVAL_STATISTICS)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user