mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 10:17:09 +00:00
Add paperless integration (#145239)
* add paperless integration - config flow and initialisation * Add first sensors - documents, inbox, storage total and available * Add status sensors with error attributes * add status coordinator and organized code * Fixed None error * Organized code and moved requests to coordinator * Organized code * optimized code * Add statustype state strings * Error handling * Organized code * Add update sensor and one coordinator for integration * add sanity sensor and timer for version request * Add sensors and icons.json. better errorhandling * Add tests and error handling * FIxed tests * Add tests for coverage * Quality scale * Stuff * Improved code structure * Removed sensor platform and reauth / reconfigure flow * bump pypaperless to 4.1.0 * Optimized tests; update sensor as update platform; little optimizations * Code optimizations with update platform * Add sensor platform * Removed update platform * quality scale * removed unused const * Removed update snapshot; better code * Changed name of entry * Fixed bugs * Minor changes * Minor changed and renamed sensors * Sensors to measurement * Fixed snapshot; test data to json; minor changes * removed mypy errors * Changed translation * minor changes * Update homeassistant/components/paperless_ngx/strings.json --------- Co-authored-by: Josef Zweck <josef@zweck.dev> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
a54c8a88ff
commit
9a8c29e05d
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@ -1140,6 +1140,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/palazzetti/ @dotvav
|
/tests/components/palazzetti/ @dotvav
|
||||||
/homeassistant/components/panel_custom/ @home-assistant/frontend
|
/homeassistant/components/panel_custom/ @home-assistant/frontend
|
||||||
/tests/components/panel_custom/ @home-assistant/frontend
|
/tests/components/panel_custom/ @home-assistant/frontend
|
||||||
|
/homeassistant/components/paperless_ngx/ @fvgarrel
|
||||||
|
/tests/components/paperless_ngx/ @fvgarrel
|
||||||
/homeassistant/components/peblar/ @frenck
|
/homeassistant/components/peblar/ @frenck
|
||||||
/tests/components/peblar/ @frenck
|
/tests/components/peblar/ @frenck
|
||||||
/homeassistant/components/peco/ @IceBotYT
|
/homeassistant/components/peco/ @IceBotYT
|
||||||
|
26
homeassistant/components/paperless_ngx/__init__.py
Normal file
26
homeassistant/components/paperless_ngx/__init__.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"""The Paperless-ngx integration."""
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .coordinator import PaperlessConfigEntry, PaperlessCoordinator
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
78
homeassistant/components/paperless_ngx/config_flow.py
Normal file
78
homeassistant/components/paperless_ngx/config_flow.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
"""Config flow for the Paperless-ngx integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pypaperless import Paperless
|
||||||
|
from pypaperless.exceptions import (
|
||||||
|
InitializationError,
|
||||||
|
PaperlessConnectionError,
|
||||||
|
PaperlessForbiddenError,
|
||||||
|
PaperlessInactiveOrDeletedError,
|
||||||
|
PaperlessInvalidTokenError,
|
||||||
|
)
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.const import CONF_API_KEY, CONF_URL
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from .const import DOMAIN, LOGGER
|
||||||
|
|
||||||
|
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_URL): str,
|
||||||
|
vol.Required(CONF_API_KEY): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PaperlessConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Paperless-ngx."""
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
self._async_abort_entries_match(
|
||||||
|
{
|
||||||
|
CONF_URL: user_input[CONF_URL],
|
||||||
|
CONF_API_KEY: user_input[CONF_API_KEY],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input is not None:
|
||||||
|
client = Paperless(
|
||||||
|
user_input[CONF_URL],
|
||||||
|
user_input[CONF_API_KEY],
|
||||||
|
session=async_get_clientsession(self.hass),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await client.initialize()
|
||||||
|
await client.statistics()
|
||||||
|
except PaperlessConnectionError:
|
||||||
|
errors[CONF_URL] = "cannot_connect"
|
||||||
|
except PaperlessInvalidTokenError:
|
||||||
|
errors[CONF_API_KEY] = "invalid_api_key"
|
||||||
|
except PaperlessInactiveOrDeletedError:
|
||||||
|
errors[CONF_API_KEY] = "user_inactive_or_deleted"
|
||||||
|
except PaperlessForbiddenError:
|
||||||
|
errors[CONF_API_KEY] = "forbidden"
|
||||||
|
except InitializationError:
|
||||||
|
errors[CONF_URL] = "cannot_connect"
|
||||||
|
except Exception as err: # noqa: BLE001
|
||||||
|
LOGGER.exception("Unexpected exception: %s", err)
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=user_input[CONF_URL], data=user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||||
|
)
|
7
homeassistant/components/paperless_ngx/const.py
Normal file
7
homeassistant/components/paperless_ngx/const.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
"""Constants for the Paperless-ngx integration."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
DOMAIN = "paperless_ngx"
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__package__)
|
109
homeassistant/components/paperless_ngx/coordinator.py
Normal file
109
homeassistant/components/paperless_ngx/coordinator.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
"""Paperless-ngx Status coordinator."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from pypaperless import Paperless
|
||||||
|
from pypaperless.exceptions import (
|
||||||
|
InitializationError,
|
||||||
|
PaperlessConnectionError,
|
||||||
|
PaperlessForbiddenError,
|
||||||
|
PaperlessInactiveOrDeletedError,
|
||||||
|
PaperlessInvalidTokenError,
|
||||||
|
)
|
||||||
|
from pypaperless.models import Statistic
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_API_KEY, CONF_URL
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import DOMAIN, LOGGER
|
||||||
|
|
||||||
|
type PaperlessConfigEntry = ConfigEntry[PaperlessCoordinator]
|
||||||
|
|
||||||
|
UPDATE_INTERVAL = 120
|
||||||
|
|
||||||
|
|
||||||
|
class PaperlessCoordinator(DataUpdateCoordinator[Statistic]):
|
||||||
|
"""Coordinator to manage Paperless-ngx statistic updates."""
|
||||||
|
|
||||||
|
config_entry: PaperlessConfigEntry
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: PaperlessConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize my coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
LOGGER,
|
||||||
|
config_entry=entry,
|
||||||
|
name="Paperless-ngx Coordinator",
|
||||||
|
update_interval=timedelta(seconds=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:
|
||||||
|
try:
|
||||||
|
await self.api.initialize()
|
||||||
|
await self.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 ConfigEntryError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="invalid_api_key",
|
||||||
|
) from err
|
||||||
|
except PaperlessInactiveOrDeletedError as err:
|
||||||
|
raise ConfigEntryError(
|
||||||
|
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
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> Statistic:
|
||||||
|
"""Fetch data from API endpoint."""
|
||||||
|
try:
|
||||||
|
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 ConfigEntryError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="invalid_api_key",
|
||||||
|
) from err
|
||||||
|
except PaperlessInactiveOrDeletedError as err:
|
||||||
|
raise ConfigEntryError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="user_inactive_or_deleted",
|
||||||
|
) from err
|
34
homeassistant/components/paperless_ngx/entity.py
Normal file
34
homeassistant/components/paperless_ngx/entity.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
"""Paperless-ngx base entity."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import EntityDescription
|
||||||
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import PaperlessCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
class PaperlessEntity(CoordinatorEntity[PaperlessCoordinator]):
|
||||||
|
"""Defines a base Paperless-ngx entity."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: PaperlessCoordinator,
|
||||||
|
description: EntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Paperless-ngx entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}"
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
entry_type=DeviceEntryType.SERVICE,
|
||||||
|
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
|
||||||
|
manufacturer="Paperless-ngx",
|
||||||
|
sw_version=coordinator.api.host_version,
|
||||||
|
configuration_url=coordinator.api.base_url,
|
||||||
|
)
|
24
homeassistant/components/paperless_ngx/icons.json
Normal file
24
homeassistant/components/paperless_ngx/icons.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"documents_total": {
|
||||||
|
"default": "mdi:file-document-multiple"
|
||||||
|
},
|
||||||
|
"documents_inbox": {
|
||||||
|
"default": "mdi:tray-full"
|
||||||
|
},
|
||||||
|
"characters_count": {
|
||||||
|
"default": "mdi:alphabet-latin"
|
||||||
|
},
|
||||||
|
"tag_count": {
|
||||||
|
"default": "mdi:tag"
|
||||||
|
},
|
||||||
|
"correspondent_count": {
|
||||||
|
"default": "mdi:account-group"
|
||||||
|
},
|
||||||
|
"document_type_count": {
|
||||||
|
"default": "mdi:format-list-bulleted-type"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
homeassistant/components/paperless_ngx/manifest.json
Normal file
12
homeassistant/components/paperless_ngx/manifest.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"domain": "paperless_ngx",
|
||||||
|
"name": "Paperless-ngx",
|
||||||
|
"codeowners": ["@fvgarrel"],
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/paperless_ngx",
|
||||||
|
"integration_type": "service",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
"loggers": ["pypaperless"],
|
||||||
|
"quality_scale": "bronze",
|
||||||
|
"requirements": ["pypaperless==4.1.0"]
|
||||||
|
}
|
78
homeassistant/components/paperless_ngx/quality_scale.yaml
Normal file
78
homeassistant/components/paperless_ngx/quality_scale.yaml
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
action-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: Integration does not register actions yet.
|
||||||
|
appropriate-polling: done
|
||||||
|
brands: done
|
||||||
|
common-modules: done
|
||||||
|
config-flow-test-coverage: done
|
||||||
|
config-flow: done
|
||||||
|
dependency-transparency: done
|
||||||
|
docs-actions:
|
||||||
|
status: exempt
|
||||||
|
comment: Integration does not register actions yet.
|
||||||
|
docs-high-level-description: done
|
||||||
|
docs-installation-instructions: done
|
||||||
|
docs-removal-instructions: done
|
||||||
|
entity-event-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: Integration does not register custom events yet.
|
||||||
|
entity-unique-id: done
|
||||||
|
has-entity-name: done
|
||||||
|
runtime-data: done
|
||||||
|
test-before-configure: done
|
||||||
|
test-before-setup: done
|
||||||
|
unique-config-entry: done
|
||||||
|
|
||||||
|
# Silver
|
||||||
|
action-exceptions:
|
||||||
|
status: exempt
|
||||||
|
comment: Integration does not register actions yet.
|
||||||
|
config-entry-unloading: done
|
||||||
|
docs-configuration-parameters:
|
||||||
|
status: exempt
|
||||||
|
comment: No options flow yet
|
||||||
|
docs-installation-parameters: done
|
||||||
|
entity-unavailable: done
|
||||||
|
integration-owner: done
|
||||||
|
log-when-unavailable: done
|
||||||
|
parallel-updates: done
|
||||||
|
reauthentication-flow: todo
|
||||||
|
test-coverage: done
|
||||||
|
|
||||||
|
# Gold
|
||||||
|
devices: done
|
||||||
|
diagnostics: todo
|
||||||
|
discovery-update-info:
|
||||||
|
status: exempt
|
||||||
|
comment: Paperless does not support discovery.
|
||||||
|
discovery:
|
||||||
|
status: exempt
|
||||||
|
comment: Paperless does not support discovery.
|
||||||
|
docs-data-update: todo
|
||||||
|
docs-examples: todo
|
||||||
|
docs-known-limitations: todo
|
||||||
|
docs-supported-devices: todo
|
||||||
|
docs-supported-functions: todo
|
||||||
|
docs-troubleshooting: todo
|
||||||
|
docs-use-cases: todo
|
||||||
|
dynamic-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: Service type integration
|
||||||
|
entity-category: done
|
||||||
|
entity-device-class: done
|
||||||
|
entity-disabled-by-default: todo
|
||||||
|
entity-translations: done
|
||||||
|
exception-translations: done
|
||||||
|
icon-translations: done
|
||||||
|
reconfiguration-flow: todo
|
||||||
|
repair-issues: todo
|
||||||
|
stale-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: Service type integration
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency: done
|
||||||
|
inject-websession: done
|
||||||
|
strict-typing: todo
|
94
homeassistant/components/paperless_ngx/sensor.py
Normal file
94
homeassistant/components/paperless_ngx/sensor.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
"""Sensor platform for Paperless-ngx."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from pypaperless.models import Statistic
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import PaperlessConfigEntry
|
||||||
|
from .entity import PaperlessEntity
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class PaperlessEntityDescription(SensorEntityDescription):
|
||||||
|
"""Describes Paperless-ngx sensor entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[Statistic], int | None]
|
||||||
|
|
||||||
|
|
||||||
|
SENSOR_DESCRIPTIONS: tuple[PaperlessEntityDescription, ...] = (
|
||||||
|
PaperlessEntityDescription(
|
||||||
|
key="documents_total",
|
||||||
|
translation_key="documents_total",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda data: data.documents_total,
|
||||||
|
),
|
||||||
|
PaperlessEntityDescription(
|
||||||
|
key="documents_inbox",
|
||||||
|
translation_key="documents_inbox",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda data: data.documents_inbox,
|
||||||
|
),
|
||||||
|
PaperlessEntityDescription(
|
||||||
|
key="characters_count",
|
||||||
|
translation_key="characters_count",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda data: data.character_count,
|
||||||
|
),
|
||||||
|
PaperlessEntityDescription(
|
||||||
|
key="tag_count",
|
||||||
|
translation_key="tag_count",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda data: data.tag_count,
|
||||||
|
),
|
||||||
|
PaperlessEntityDescription(
|
||||||
|
key="correspondent_count",
|
||||||
|
translation_key="correspondent_count",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda data: data.correspondent_count,
|
||||||
|
),
|
||||||
|
PaperlessEntityDescription(
|
||||||
|
key="document_type_count",
|
||||||
|
translation_key="document_type_count",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda data: data.document_type_count,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: PaperlessConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Paperless-ngx sensors."""
|
||||||
|
async_add_entities(
|
||||||
|
PaperlessSensor(
|
||||||
|
coordinator=entry.runtime_data,
|
||||||
|
description=sensor_description,
|
||||||
|
)
|
||||||
|
for sensor_description in SENSOR_DESCRIPTIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PaperlessSensor(PaperlessEntity, SensorEntity):
|
||||||
|
"""Defines a Paperless-ngx sensor entity."""
|
||||||
|
|
||||||
|
entity_description: PaperlessEntityDescription
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> int | None:
|
||||||
|
"""Return the current value of the sensor."""
|
||||||
|
return self.entity_description.value_fn(self.coordinator.data)
|
72
homeassistant/components/paperless_ngx/strings.json
Normal file
72
homeassistant/components/paperless_ngx/strings.json
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"url": "[%key:common::config_flow::data::url%]",
|
||||||
|
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"url": "URL to connect to the Paperless-ngx instance",
|
||||||
|
"api_key": "API key to connect to the Paperless-ngx API"
|
||||||
|
},
|
||||||
|
"title": "Add Paperless-ngx instance"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::invalid_host%]",
|
||||||
|
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
|
||||||
|
"user_inactive_or_deleted": "Authentication failed. The user is inactive or has been deleted.",
|
||||||
|
"forbidden": "The token does not have permission to access the API.",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"documents_total": {
|
||||||
|
"name": "Total documents",
|
||||||
|
"unit_of_measurement": "documents"
|
||||||
|
},
|
||||||
|
"documents_inbox": {
|
||||||
|
"name": "Documents in inbox",
|
||||||
|
"unit_of_measurement": "[%key:component::paperless_ngx::entity::sensor::documents_total::unit_of_measurement%]"
|
||||||
|
},
|
||||||
|
"characters_count": {
|
||||||
|
"name": "Total characters",
|
||||||
|
"unit_of_measurement": "characters"
|
||||||
|
},
|
||||||
|
"tag_count": {
|
||||||
|
"name": "Tags",
|
||||||
|
"unit_of_measurement": "tags"
|
||||||
|
},
|
||||||
|
"correspondent_count": {
|
||||||
|
"name": "Correspondents",
|
||||||
|
"unit_of_measurement": "correspondents"
|
||||||
|
},
|
||||||
|
"document_type_count": {
|
||||||
|
"name": "Document types",
|
||||||
|
"unit_of_measurement": "document types"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"cannot_connect": {
|
||||||
|
"message": "[%key:common::config_flow::error::invalid_host%]"
|
||||||
|
},
|
||||||
|
"invalid_api_key": {
|
||||||
|
"message": "[%key:common::config_flow::error::invalid_api_key%]"
|
||||||
|
},
|
||||||
|
"user_inactive_or_deleted": {
|
||||||
|
"message": "[%key:component::paperless_ngx::config::error::user_inactive_or_deleted%]"
|
||||||
|
},
|
||||||
|
"forbidden": {
|
||||||
|
"message": "[%key:component::paperless_ngx::config::error::forbidden%]"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"message": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@ -469,6 +469,7 @@ FLOWS = {
|
|||||||
"p1_monitor",
|
"p1_monitor",
|
||||||
"palazzetti",
|
"palazzetti",
|
||||||
"panasonic_viera",
|
"panasonic_viera",
|
||||||
|
"paperless_ngx",
|
||||||
"peblar",
|
"peblar",
|
||||||
"peco",
|
"peco",
|
||||||
"pegel_online",
|
"pegel_online",
|
||||||
|
@ -4848,6 +4848,12 @@
|
|||||||
"config_flow": false,
|
"config_flow": false,
|
||||||
"iot_class": "local_polling"
|
"iot_class": "local_polling"
|
||||||
},
|
},
|
||||||
|
"paperless_ngx": {
|
||||||
|
"name": "Paperless-ngx",
|
||||||
|
"integration_type": "service",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "local_polling"
|
||||||
|
},
|
||||||
"pcs_lighting": {
|
"pcs_lighting": {
|
||||||
"name": "PCS Lighting",
|
"name": "PCS Lighting",
|
||||||
"integration_type": "virtual",
|
"integration_type": "virtual",
|
||||||
|
3
requirements_all.txt
generated
3
requirements_all.txt
generated
@ -2226,6 +2226,9 @@ pyownet==0.10.0.post1
|
|||||||
# homeassistant.components.palazzetti
|
# homeassistant.components.palazzetti
|
||||||
pypalazzetti==0.1.19
|
pypalazzetti==0.1.19
|
||||||
|
|
||||||
|
# homeassistant.components.paperless_ngx
|
||||||
|
pypaperless==4.1.0
|
||||||
|
|
||||||
# homeassistant.components.elv
|
# homeassistant.components.elv
|
||||||
pypca==0.0.7
|
pypca==0.0.7
|
||||||
|
|
||||||
|
3
requirements_test_all.txt
generated
3
requirements_test_all.txt
generated
@ -1823,6 +1823,9 @@ pyownet==0.10.0.post1
|
|||||||
# homeassistant.components.palazzetti
|
# homeassistant.components.palazzetti
|
||||||
pypalazzetti==0.1.19
|
pypalazzetti==0.1.19
|
||||||
|
|
||||||
|
# homeassistant.components.paperless_ngx
|
||||||
|
pypaperless==4.1.0
|
||||||
|
|
||||||
# homeassistant.components.lcn
|
# homeassistant.components.lcn
|
||||||
pypck==0.8.6
|
pypck==0.8.6
|
||||||
|
|
||||||
|
14
tests/components/paperless_ngx/__init__.py
Normal file
14
tests/components/paperless_ngx/__init__.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"""Tests for the Paperless-ngx integration."""
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_integration(
|
||||||
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Set up the Paperless-ngx integration for testing."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
76
tests/components/paperless_ngx/conftest.py
Normal file
76
tests/components/paperless_ngx/conftest.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
"""Common fixtures for the Paperless-ngx tests."""
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
import json
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
from pypaperless.models import Statistic
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.paperless_ngx.const import DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
from .const import USER_INPUT
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, load_fixture
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_statistic_data() -> Generator[MagicMock]:
|
||||||
|
"""Return test statistic data."""
|
||||||
|
return json.loads(load_fixture("test_data_statistic.json", DOMAIN))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_statistic_data_update() -> Generator[MagicMock]:
|
||||||
|
"""Return updated test statistic data."""
|
||||||
|
return json.loads(load_fixture("test_data_statistic_update.json", DOMAIN))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_paperless(mock_statistic_data: MagicMock) -> Generator[AsyncMock]:
|
||||||
|
"""Mock the pypaperless.Paperless client."""
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.paperless_ngx.coordinator.Paperless",
|
||||||
|
autospec=True,
|
||||||
|
) as paperless_mock,
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.paperless_ngx.config_flow.Paperless",
|
||||||
|
new=paperless_mock,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
paperless = paperless_mock.return_value
|
||||||
|
|
||||||
|
paperless.base_url = "http://paperless.example.com/"
|
||||||
|
paperless.host_version = "2.3.0"
|
||||||
|
paperless.initialize.return_value = None
|
||||||
|
paperless.statistics = AsyncMock(
|
||||||
|
return_value=Statistic.create_with_data(
|
||||||
|
paperless, data=mock_statistic_data, fetched=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
yield paperless
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config_entry() -> MockConfigEntry:
|
||||||
|
"""Return the default mocked config entry."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
entry_id="paperless_ngx_test",
|
||||||
|
title="Paperless-ngx",
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def init_integration(
|
||||||
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_paperless: MagicMock
|
||||||
|
) -> MockConfigEntry:
|
||||||
|
"""Set up the Paperless-ngx integration for testing."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
return mock_config_entry
|
8
tests/components/paperless_ngx/const.py
Normal file
8
tests/components/paperless_ngx/const.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
"""Constants for the Paperless NGX integration tests."""
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_API_KEY, CONF_URL
|
||||||
|
|
||||||
|
USER_INPUT = {
|
||||||
|
CONF_URL: "https://192.168.69.16:8000",
|
||||||
|
CONF_API_KEY: "test_token",
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"documents_total": 999,
|
||||||
|
"documents_inbox": 9,
|
||||||
|
"inbox_tag": 9,
|
||||||
|
"inbox_tags": [9],
|
||||||
|
"document_file_type_counts": [
|
||||||
|
{ "mime_type": "application/pdf", "mime_type_count": 998 },
|
||||||
|
{ "mime_type": "image/png", "mime_type_count": 1 }
|
||||||
|
],
|
||||||
|
"character_count": 99999,
|
||||||
|
"tag_count": 99,
|
||||||
|
"correspondent_count": 99,
|
||||||
|
"document_type_count": 99,
|
||||||
|
"storage_path_count": 9,
|
||||||
|
"current_asn": 99
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"documents_total": 420,
|
||||||
|
"documents_inbox": 3,
|
||||||
|
"inbox_tag": 5,
|
||||||
|
"inbox_tags": [2],
|
||||||
|
"document_file_type_counts": [
|
||||||
|
{ "mime_type": "application/pdf", "mime_type_count": 419 },
|
||||||
|
{ "mime_type": "image/png", "mime_type_count": 1 }
|
||||||
|
],
|
||||||
|
"character_count": 324234,
|
||||||
|
"tag_count": 43,
|
||||||
|
"correspondent_count": 9659,
|
||||||
|
"document_type_count": 54656,
|
||||||
|
"storage_path_count": 6459,
|
||||||
|
"current_asn": 959
|
||||||
|
}
|
307
tests/components/paperless_ngx/snapshots/test_sensor.ambr
Normal file
307
tests/components/paperless_ngx/snapshots/test_sensor.ambr
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_sensor_platfom[sensor.paperless_ngx_correspondents-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': None,
|
||||||
|
'entity_id': 'sensor.paperless_ngx_correspondents',
|
||||||
|
'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': 'Correspondents',
|
||||||
|
'platform': 'paperless_ngx',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'correspondent_count',
|
||||||
|
'unique_id': 'paperless_ngx_test_correspondent_count',
|
||||||
|
'unit_of_measurement': 'correspondents',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor_platfom[sensor.paperless_ngx_correspondents-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Paperless-ngx Correspondents',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': 'correspondents',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.paperless_ngx_correspondents',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '99',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor_platfom[sensor.paperless_ngx_document_types-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': None,
|
||||||
|
'entity_id': 'sensor.paperless_ngx_document_types',
|
||||||
|
'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': 'Document types',
|
||||||
|
'platform': 'paperless_ngx',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'document_type_count',
|
||||||
|
'unique_id': 'paperless_ngx_test_document_type_count',
|
||||||
|
'unit_of_measurement': 'document types',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor_platfom[sensor.paperless_ngx_document_types-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Paperless-ngx Document types',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': 'document types',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.paperless_ngx_document_types',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '99',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor_platfom[sensor.paperless_ngx_documents_in_inbox-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': None,
|
||||||
|
'entity_id': 'sensor.paperless_ngx_documents_in_inbox',
|
||||||
|
'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': 'Documents in inbox',
|
||||||
|
'platform': 'paperless_ngx',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'documents_inbox',
|
||||||
|
'unique_id': 'paperless_ngx_test_documents_inbox',
|
||||||
|
'unit_of_measurement': 'documents',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor_platfom[sensor.paperless_ngx_documents_in_inbox-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Paperless-ngx Documents in inbox',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': 'documents',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.paperless_ngx_documents_in_inbox',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '9',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor_platfom[sensor.paperless_ngx_tags-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': None,
|
||||||
|
'entity_id': 'sensor.paperless_ngx_tags',
|
||||||
|
'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': 'Tags',
|
||||||
|
'platform': 'paperless_ngx',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'tag_count',
|
||||||
|
'unique_id': 'paperless_ngx_test_tag_count',
|
||||||
|
'unit_of_measurement': 'tags',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor_platfom[sensor.paperless_ngx_tags-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Paperless-ngx Tags',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': 'tags',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.paperless_ngx_tags',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '99',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor_platfom[sensor.paperless_ngx_total_characters-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': None,
|
||||||
|
'entity_id': 'sensor.paperless_ngx_total_characters',
|
||||||
|
'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 characters',
|
||||||
|
'platform': 'paperless_ngx',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'characters_count',
|
||||||
|
'unique_id': 'paperless_ngx_test_characters_count',
|
||||||
|
'unit_of_measurement': 'characters',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor_platfom[sensor.paperless_ngx_total_characters-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Paperless-ngx Total characters',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': 'characters',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.paperless_ngx_total_characters',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '99999',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor_platfom[sensor.paperless_ngx_total_documents-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': None,
|
||||||
|
'entity_id': 'sensor.paperless_ngx_total_documents',
|
||||||
|
'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 documents',
|
||||||
|
'platform': 'paperless_ngx',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'documents_total',
|
||||||
|
'unique_id': 'paperless_ngx_test_documents_total',
|
||||||
|
'unit_of_measurement': 'documents',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor_platfom[sensor.paperless_ngx_total_documents-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Paperless-ngx Total documents',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': 'documents',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.paperless_ngx_total_documents',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '999',
|
||||||
|
})
|
||||||
|
# ---
|
112
tests/components/paperless_ngx/test_config_flow.py
Normal file
112
tests/components/paperless_ngx/test_config_flow.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
"""Tests for the Paperless-ngx config flow."""
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from pypaperless.exceptions import (
|
||||||
|
InitializationError,
|
||||||
|
PaperlessConnectionError,
|
||||||
|
PaperlessForbiddenError,
|
||||||
|
PaperlessInactiveOrDeletedError,
|
||||||
|
PaperlessInvalidTokenError,
|
||||||
|
)
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.paperless_ngx.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_USER
|
||||||
|
from homeassistant.const import CONF_API_KEY, CONF_URL
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
|
from .const import USER_INPUT
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, patch
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||||
|
"""Override async_setup_entry."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.paperless_ngx.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup_entry:
|
||||||
|
yield mock_setup_entry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_full_config_flow(hass: HomeAssistant) -> None:
|
||||||
|
"""Test registering an integration and finishing flow works."""
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["flow_id"]
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
config_entry = result["result"]
|
||||||
|
assert config_entry.title == USER_INPUT[CONF_URL]
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert config_entry.data == USER_INPUT
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("side_effect", "expected_error"),
|
||||||
|
[
|
||||||
|
(PaperlessConnectionError(), {CONF_URL: "cannot_connect"}),
|
||||||
|
(PaperlessInvalidTokenError(), {CONF_API_KEY: "invalid_api_key"}),
|
||||||
|
(PaperlessInactiveOrDeletedError(), {CONF_API_KEY: "user_inactive_or_deleted"}),
|
||||||
|
(PaperlessForbiddenError(), {CONF_API_KEY: "forbidden"}),
|
||||||
|
(InitializationError(), {CONF_URL: "cannot_connect"}),
|
||||||
|
(Exception("BOOM!"), {"base": "unknown"}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_config_flow_error_handling(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_paperless: AsyncMock,
|
||||||
|
side_effect: Exception,
|
||||||
|
expected_error: dict[str, str],
|
||||||
|
) -> None:
|
||||||
|
"""Test user step shows correct error for various client initialization issues."""
|
||||||
|
mock_paperless.initialize.side_effect = side_effect
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data=USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == expected_error
|
||||||
|
|
||||||
|
mock_paperless.initialize.side_effect = None
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == USER_INPUT[CONF_URL]
|
||||||
|
assert result["data"] == USER_INPUT
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_already_exists(
|
||||||
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Test we only allow a single config flow."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
data=USER_INPUT,
|
||||||
|
context={"source": config_entries.SOURCE_USER},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
65
tests/components/paperless_ngx/test_init.py
Normal file
65
tests/components/paperless_ngx/test_init.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"""Test the Paperless-ngx integration initialization."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from pypaperless.exceptions import (
|
||||||
|
InitializationError,
|
||||||
|
PaperlessConnectionError,
|
||||||
|
PaperlessForbiddenError,
|
||||||
|
PaperlessInactiveOrDeletedError,
|
||||||
|
PaperlessInvalidTokenError,
|
||||||
|
)
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_load_unload_config_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test loading and unloading the integration."""
|
||||||
|
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),
|
||||||
|
(PaperlessInvalidTokenError(), ConfigEntryState.SETUP_ERROR, "invalid_api_key"),
|
||||||
|
(
|
||||||
|
PaperlessInactiveOrDeletedError(),
|
||||||
|
ConfigEntryState.SETUP_ERROR,
|
||||||
|
"user_inactive_or_deleted",
|
||||||
|
),
|
||||||
|
(PaperlessForbiddenError(), ConfigEntryState.SETUP_ERROR, "forbidden"),
|
||||||
|
(InitializationError(), ConfigEntryState.SETUP_ERROR, "cannot_connect"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_setup_config_error_handling(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_paperless: AsyncMock,
|
||||||
|
side_effect: Exception,
|
||||||
|
expected_state: ConfigEntryState,
|
||||||
|
expected_error_key: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test all initialization error paths during setup."""
|
||||||
|
mock_paperless.initialize.side_effect = side_effect
|
||||||
|
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
assert mock_config_entry.state == expected_state
|
||||||
|
assert mock_config_entry.error_reason_translation_key == expected_error_key
|
111
tests/components/paperless_ngx/test_sensor.py
Normal file
111
tests/components/paperless_ngx/test_sensor.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
"""Tests for Paperless-ngx sensor platform."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
from pypaperless.exceptions import (
|
||||||
|
PaperlessConnectionError,
|
||||||
|
PaperlessForbiddenError,
|
||||||
|
PaperlessInactiveOrDeletedError,
|
||||||
|
PaperlessInvalidTokenError,
|
||||||
|
)
|
||||||
|
from pypaperless.models import Statistic
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
|
||||||
|
from tests.common import (
|
||||||
|
AsyncMock,
|
||||||
|
MockConfigEntry,
|
||||||
|
SnapshotAssertion,
|
||||||
|
async_fire_time_changed,
|
||||||
|
patch,
|
||||||
|
snapshot_platform,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sensor_platfom(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test paperless_ngx update sensors."""
|
||||||
|
with patch("homeassistant.components.paperless_ngx.PLATFORMS", [Platform.SENSOR]):
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_integration")
|
||||||
|
async def test_statistic_sensor_state(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_paperless: AsyncMock,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
mock_statistic_data_update,
|
||||||
|
) -> None:
|
||||||
|
"""Ensure sensor entities are added automatically."""
|
||||||
|
# initialize with 999 documents
|
||||||
|
state = hass.states.get("sensor.paperless_ngx_total_documents")
|
||||||
|
assert state.state == "999"
|
||||||
|
|
||||||
|
# update to 420 documents
|
||||||
|
mock_paperless.statistics = AsyncMock(
|
||||||
|
return_value=Statistic.create_with_data(
|
||||||
|
mock_paperless, data=mock_statistic_data_update, fetched=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
freezer.tick(timedelta(seconds=120))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.paperless_ngx_total_documents")
|
||||||
|
assert state.state == "420"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_integration")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"error_cls",
|
||||||
|
[
|
||||||
|
PaperlessForbiddenError,
|
||||||
|
PaperlessConnectionError,
|
||||||
|
PaperlessInactiveOrDeletedError,
|
||||||
|
PaperlessInvalidTokenError,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test__statistic_sensor_state_on_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_paperless: AsyncMock,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
mock_statistic_data_update,
|
||||||
|
error_cls,
|
||||||
|
) -> None:
|
||||||
|
"""Ensure sensor entities are added automatically."""
|
||||||
|
# simulate error
|
||||||
|
mock_paperless.statistics.side_effect = error_cls
|
||||||
|
|
||||||
|
freezer.tick(timedelta(seconds=120))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.paperless_ngx_total_documents")
|
||||||
|
assert state.state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
# recover from error
|
||||||
|
mock_paperless.statistics = AsyncMock(
|
||||||
|
return_value=Statistic.create_with_data(
|
||||||
|
mock_paperless, data=mock_statistic_data_update, fetched=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
freezer.tick(timedelta(seconds=120))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.paperless_ngx_total_documents")
|
||||||
|
assert state.state == "420"
|
Loading…
x
Reference in New Issue
Block a user