2025-03-26 08:53:46 +01:00

150 lines
5.0 KiB
Python

"""Utility functions for the Reolink component."""
from __future__ import annotations
from collections.abc import Awaitable, Callable, Coroutine
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from reolink_aio.exceptions import (
ApiError,
CredentialsInvalidError,
InvalidContentTypeError,
InvalidParameterError,
LoginError,
NoDataError,
NotSupportedError,
ReolinkConnectionError,
ReolinkError,
ReolinkTimeoutError,
SubscriptionError,
UnexpectedDataError,
)
from homeassistant import config_entries
from homeassistant.components.media_source import Unresolvable
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.storage import Store
from homeassistant.helpers.translation import async_get_exception_message
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN
if TYPE_CHECKING:
from .host import ReolinkHost
STORAGE_VERSION = 1
type ReolinkConfigEntry = config_entries.ConfigEntry[ReolinkData]
@dataclass
class ReolinkData:
"""Data for the Reolink integration."""
host: ReolinkHost
device_coordinator: DataUpdateCoordinator[None]
firmware_coordinator: DataUpdateCoordinator[None]
def is_connected(hass: HomeAssistant, config_entry: config_entries.ConfigEntry) -> bool:
"""Check if an existing entry has a proper connection."""
return (
hasattr(config_entry, "runtime_data")
and config_entry.state == config_entries.ConfigEntryState.LOADED
and config_entry.runtime_data.device_coordinator.last_update_success
)
def get_host(hass: HomeAssistant, config_entry_id: str) -> ReolinkHost:
"""Return the Reolink host from the config entry id."""
config_entry: ReolinkConfigEntry | None = hass.config_entries.async_get_entry(
config_entry_id
)
if config_entry is None:
raise Unresolvable(
f"Could not find Reolink config entry id '{config_entry_id}'."
)
return config_entry.runtime_data.host
def get_store(hass: HomeAssistant, config_entry_id: str) -> Store[str]:
"""Return the reolink store."""
return Store[str](hass, STORAGE_VERSION, f"{DOMAIN}.{config_entry_id}.json")
def get_device_uid_and_ch(
device: dr.DeviceEntry, host: ReolinkHost
) -> tuple[list[str], int | None, bool]:
"""Get the channel and the split device_uid from a reolink DeviceEntry."""
device_uid = [
dev_id[1].split("_") for dev_id in device.identifiers if dev_id[0] == DOMAIN
][0]
is_chime = False
if len(device_uid) < 2:
# NVR itself
ch = None
elif device_uid[1].startswith("ch") and len(device_uid[1]) <= 5:
ch = int(device_uid[1][2:])
elif device_uid[1].startswith("chime"):
ch = int(device_uid[1][5:])
is_chime = True
else:
device_uid_part = "_".join(device_uid[1:])
ch = host.api.channel_for_uid(device_uid_part)
return (device_uid, ch, is_chime)
def check_translation_key(err: ReolinkError) -> str | None:
"""Check if the translation key from the upstream library is present."""
if not err.translation_key:
return None
if async_get_exception_message(DOMAIN, err.translation_key) == err.translation_key:
# translation key not found in strings.json
return None
return err.translation_key
_EXCEPTION_TO_TRANSLATION_KEY = {
ApiError: "api_error",
InvalidContentTypeError: "invalid_content_type",
CredentialsInvalidError: "invalid_credentials",
LoginError: "login_error",
NoDataError: "no_data",
UnexpectedDataError: "unexpected_data",
NotSupportedError: "not_supported",
SubscriptionError: "subscription_error",
ReolinkConnectionError: "connection_error",
ReolinkTimeoutError: "timeout",
}
# Decorators
def raise_translated_error[**P, R](
func: Callable[P, Awaitable[R]],
) -> Callable[P, Coroutine[Any, Any, R]]:
"""Wrap a reolink-aio function to translate any potential errors."""
async def decorator_raise_translated_error(*args: P.args, **kwargs: P.kwargs) -> R:
"""Try a reolink-aio function and translate any potential errors."""
try:
return await func(*args, **kwargs)
except InvalidParameterError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key=check_translation_key(err) or "invalid_parameter",
translation_placeholders={"err": str(err)},
) from err
except ReolinkError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key=check_translation_key(err)
or _EXCEPTION_TO_TRANSLATION_KEY.get(type(err), "unexpected"),
translation_placeholders={"err": str(err)},
) from err
return decorator_raise_translated_error