Merge otto/ha-client-errors into main
This commit is contained in:
@@ -5,13 +5,25 @@ from dataclasses import dataclass
|
||||
|
||||
import requests
|
||||
|
||||
from app.ha.exceptions import HaAuthError, HaHttpError, HaTimeoutError, HaUnexpectedPayloadError
|
||||
from app.ha.exceptions import (
|
||||
HaAuthError,
|
||||
HaHttpError,
|
||||
HaTimeoutError,
|
||||
HaUnexpectedPayloadError,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HaClientSettings:
|
||||
url: str
|
||||
token: str
|
||||
timeout_seconds: int = 10
|
||||
|
||||
|
||||
class HaClient:
|
||||
def __init__(self, settings) -> None:
|
||||
def __init__(self, settings: HaClientSettings) -> None:
|
||||
self._settings = settings
|
||||
self._session = requests.Session()
|
||||
self._session.headers.update({
|
||||
@@ -19,35 +31,44 @@ class HaClient:
|
||||
"Content-Type": "application/json",
|
||||
})
|
||||
|
||||
def list_entities(self):
|
||||
url = f"{self._settings.url}/api/states"
|
||||
def list_entities(self) -> list[dict[str, object]]:
|
||||
try:
|
||||
response = self._session.get(
|
||||
url,
|
||||
f"{self._settings.url}/api/states",
|
||||
timeout=self._settings.timeout_seconds,
|
||||
)
|
||||
except requests.Timeout as exc:
|
||||
raise HaTimeoutError("Zeitüberschreitung beim Zugriff auf Home Assistant.") from exc
|
||||
except requests.RequestException as exc:
|
||||
raise HaHttpError(
|
||||
getattr(exc.response, "status_code", 502),
|
||||
getattr(getattr(exc, "response", None), "status_code", 502),
|
||||
"Netzwerkfehler beim Zugriff auf Home Assistant.",
|
||||
) from exc
|
||||
|
||||
if response.status_code in (401, 403):
|
||||
raise HaAuthError(response.status_code, "Authentifizierung bei Home Assistant fehlgeschlagen.")
|
||||
raise HaAuthError(
|
||||
response.status_code,
|
||||
"Authentifizierung bei Home Assistant fehlgeschlagen.",
|
||||
)
|
||||
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.HTTPError as exc:
|
||||
raise HaHttpError(response.status_code, "Home Assistant meldet einen Fehler.") from exc
|
||||
raise HaHttpError(
|
||||
response.status_code,
|
||||
"Home Assistant meldet einen Fehler.",
|
||||
) from exc
|
||||
|
||||
try:
|
||||
payload = response.json()
|
||||
except ValueError as exc:
|
||||
raise HaUnexpectedPayloadError("Antwort von Home Assistant ist kein gültiges JSON.") from exc
|
||||
raise HaUnexpectedPayloadError(
|
||||
"Antwort von Home Assistant ist kein gültiges JSON."
|
||||
) from exc
|
||||
|
||||
if not isinstance(payload, list):
|
||||
raise HaUnexpectedPayloadError("Antwort von Home Assistant hat unerwartetes Format.")
|
||||
raise HaUnexpectedPayloadError(
|
||||
"Antwort von Home Assistant hat unerwartetes Format."
|
||||
)
|
||||
|
||||
return payload
|
||||
@@ -4,14 +4,20 @@ from __future__ import annotations
|
||||
class HaClientError(Exception):
|
||||
"""Basisklasse für HA-Client-Fehler."""
|
||||
|
||||
public_detail: str | None = None
|
||||
|
||||
|
||||
class HaTimeoutError(HaClientError):
|
||||
"""Zeitüberschreitung bei Request an Home Assistant."""
|
||||
|
||||
public_detail = "Home Assistant request timed out."
|
||||
|
||||
|
||||
class HaHttpError(HaClientError):
|
||||
"""Nicht erfolgreicher HTTP-Statuscode."""
|
||||
|
||||
public_detail = "Home Assistant request failed."
|
||||
|
||||
def __init__(self, status_code: int, message: str = "") -> None:
|
||||
super().__init__(message)
|
||||
self.status_code = status_code
|
||||
@@ -20,6 +26,10 @@ class HaHttpError(HaClientError):
|
||||
class HaAuthError(HaHttpError):
|
||||
"""Authentifizierung oder Berechtigung fehlgeschlagen."""
|
||||
|
||||
public_detail = "Home Assistant authentication failed."
|
||||
|
||||
|
||||
class HaUnexpectedPayloadError(HaClientError):
|
||||
"""Antwort hat nicht das erwartete Format."""
|
||||
"""Antwort hat nicht das erwartete Format."""
|
||||
|
||||
public_detail = "Home Assistant returned an unexpected payload."
|
||||
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from app.ha.client import HaClient
|
||||
from app.ha.models import HaEntitySummary, HaState
|
||||
from app.ha.models import HaEntitySummary
|
||||
|
||||
|
||||
class HaReader:
|
||||
@@ -18,14 +19,21 @@ class HaReader:
|
||||
if "." not in entity_id:
|
||||
continue
|
||||
domain = entity_id.split(".", 1)[0]
|
||||
attributes = item.get("attributes") or {}
|
||||
raw_attributes = item.get("attributes") or {}
|
||||
attributes: dict[str, Any] = raw_attributes if isinstance(raw_attributes, dict) else {}
|
||||
summaries.append(
|
||||
HaEntitySummary(
|
||||
entity_id=entity_id,
|
||||
domain=domain,
|
||||
state_class=str(attributes.get("state_class") or ""),
|
||||
device_class=str(attributes.get("device_class") or ""),
|
||||
unit_of_measurement=str(attributes.get("unit_of_measurement") or ""),
|
||||
state_class=_optional_str(attributes.get("state_class")),
|
||||
device_class=_optional_str(attributes.get("device_class")),
|
||||
unit_of_measurement=_optional_str(attributes.get("unit_of_measurement")),
|
||||
)
|
||||
)
|
||||
return summaries
|
||||
|
||||
|
||||
def _optional_str(value: object) -> str | None:
|
||||
if value is None or value == "":
|
||||
return None
|
||||
return str(value)
|
||||
|
||||
Reference in New Issue
Block a user