BEHAVIOR-001: learn and predict actuator actions
This commit is contained in:
@@ -5,6 +5,7 @@ from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
import json
|
||||
import re
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
@@ -19,6 +20,7 @@ from app.ha.exceptions import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_ENTITY_ID_PATTERN = re.compile(r"^[a-z0-9_]+\.[a-z0-9_]+$")
|
||||
_SERVICE_PART_PATTERN = re.compile(r"^[a-z0-9_]+$")
|
||||
_MAX_HISTORY_SECONDS = 31 * 24 * 60 * 60
|
||||
|
||||
|
||||
@@ -84,6 +86,44 @@ class HaClient:
|
||||
)
|
||||
return payload
|
||||
|
||||
def get_logbook(
|
||||
self,
|
||||
entity_id: str,
|
||||
start_time: datetime,
|
||||
end_time: datetime,
|
||||
) -> list[object]:
|
||||
self._validate_period([entity_id], start_time, end_time)
|
||||
start = quote(start_time.isoformat(), safe=":+")
|
||||
payload = self._get_json(
|
||||
f"/api/logbook/{start}",
|
||||
params={
|
||||
"entity": entity_id,
|
||||
"end_time": end_time.isoformat(),
|
||||
},
|
||||
)
|
||||
if not isinstance(payload, list):
|
||||
raise HaUnexpectedPayloadError(
|
||||
"Logbook-Antwort von Home Assistant hat unerwartetes Format."
|
||||
)
|
||||
return payload
|
||||
|
||||
def call_service(
|
||||
self,
|
||||
domain: str,
|
||||
service: str,
|
||||
service_data: dict[str, object],
|
||||
) -> list[object]:
|
||||
if not _SERVICE_PART_PATTERN.fullmatch(domain):
|
||||
raise ValueError("Ungültige Service-Domain.")
|
||||
if not _SERVICE_PART_PATTERN.fullmatch(service):
|
||||
raise ValueError("Ungültiger Service-Name.")
|
||||
payload = self._post_json(f"/api/services/{domain}/{service}", service_data)
|
||||
if not isinstance(payload, list):
|
||||
raise HaUnexpectedPayloadError(
|
||||
"Service-Antwort von Home Assistant hat unerwartetes Format."
|
||||
)
|
||||
return payload
|
||||
|
||||
def list_entity_metadata(self, entity_ids: list[str]) -> dict[str, dict[str, str | None]]:
|
||||
if not entity_ids:
|
||||
return {}
|
||||
@@ -153,6 +193,36 @@ class HaClient:
|
||||
|
||||
return payload
|
||||
|
||||
def _post_json(self, path: str, payload: Any) -> object:
|
||||
try:
|
||||
response = self._session.post(
|
||||
f"{self._settings.url.rstrip('/')}{path}",
|
||||
json=payload,
|
||||
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(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.",
|
||||
)
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.HTTPError as exc:
|
||||
raise HaHttpError(response.status_code, "Home Assistant meldet einen Fehler.") from exc
|
||||
try:
|
||||
return response.json()
|
||||
except ValueError as exc:
|
||||
raise HaUnexpectedPayloadError(
|
||||
"Antwort von Home Assistant ist kein gültiges JSON."
|
||||
) from exc
|
||||
|
||||
def _post_text(self, path: str, payload: dict[str, str]) -> str:
|
||||
try:
|
||||
response = self._session.post(
|
||||
@@ -179,6 +249,25 @@ class HaClient:
|
||||
raise HaHttpError(response.status_code, "Home Assistant meldet einen Fehler.") from exc
|
||||
return response.text
|
||||
|
||||
@staticmethod
|
||||
def _validate_period(
|
||||
entity_ids: list[str],
|
||||
start_time: datetime,
|
||||
end_time: datetime,
|
||||
) -> None:
|
||||
if not entity_ids:
|
||||
raise ValueError("Mindestens eine entity_id ist erforderlich.")
|
||||
if len(entity_ids) > 100:
|
||||
raise ValueError("Es können höchstens 100 Entities abgefragt werden.")
|
||||
if any(not _ENTITY_ID_PATTERN.fullmatch(entity_id) for entity_id in entity_ids):
|
||||
raise ValueError("entity_id enthält ein ungültiges Format.")
|
||||
if start_time.tzinfo is None or end_time.tzinfo is None:
|
||||
raise ValueError("start_time und end_time müssen eine Zeitzone enthalten.")
|
||||
if end_time <= start_time:
|
||||
raise ValueError("end_time muss nach start_time liegen.")
|
||||
if (end_time - start_time).total_seconds() > _MAX_HISTORY_SECONDS:
|
||||
raise ValueError("History-Abfragen sind auf 31 Tage begrenzt.")
|
||||
|
||||
|
||||
def _metadata_template(entity_ids: list[str]) -> str:
|
||||
ids = json.dumps(entity_ids, ensure_ascii=True)
|
||||
|
||||
Reference in New Issue
Block a user