Add Home Assistant outbound adapter
This commit is contained in:
@@ -22,6 +22,7 @@ class Settings(BaseSettings):
|
||||
|
||||
home_assistant_base_url: str = ""
|
||||
home_assistant_auth_token: str = ""
|
||||
home_assistant_timeout_seconds: float = 1.0
|
||||
home_assistant_action_task_project_id: str = ""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
|
||||
@@ -1,12 +1,108 @@
|
||||
from dataclasses import dataclass
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
from urllib import error, parse, request
|
||||
|
||||
from app.config import Settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
SUCCESS_STATUS_CODES = {200, 201}
|
||||
|
||||
|
||||
class HomeAssistantConfigError(RuntimeError):
|
||||
"""Raised when required Home Assistant outbound configuration is missing."""
|
||||
|
||||
|
||||
class HomeAssistantRequestError(RuntimeError):
|
||||
"""Raised when a Home Assistant outbound HTTP request fails."""
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class HomeAssistantClient:
|
||||
settings: Settings
|
||||
timeout_seconds: float | None = field(default=None)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.timeout_seconds is None:
|
||||
self.timeout_seconds = self.settings.home_assistant_timeout_seconds
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
return bool(self.settings.home_assistant_base_url and self.settings.home_assistant_auth_token)
|
||||
|
||||
def publish_sensor(
|
||||
self,
|
||||
*,
|
||||
entity_id: str,
|
||||
state: str,
|
||||
attributes: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
self._require_config()
|
||||
if not entity_id:
|
||||
raise ValueError("entity_id must not be empty")
|
||||
|
||||
payload = {
|
||||
"entity_id": entity_id,
|
||||
"state": state,
|
||||
"attributes": attributes or {},
|
||||
}
|
||||
self._post_json(f"/api/states/{entity_id}", payload, operation="publish_sensor")
|
||||
|
||||
def trigger_webhook(self, *, webhook_id: str, body: Any) -> None:
|
||||
self._require_config()
|
||||
if not webhook_id:
|
||||
raise ValueError("webhook_id must not be empty")
|
||||
|
||||
self._post_json(f"/api/webhook/{webhook_id}", body, operation="trigger_webhook")
|
||||
|
||||
def _require_config(self) -> None:
|
||||
if self.is_configured():
|
||||
return
|
||||
raise HomeAssistantConfigError(
|
||||
"Home Assistant outbound integration is not configured. "
|
||||
"Set HOME_ASSISTANT_BASE_URL and HOME_ASSISTANT_AUTH_TOKEN."
|
||||
)
|
||||
|
||||
def _post_json(self, path: str, payload: Any, *, operation: str) -> None:
|
||||
url = self._build_url(path)
|
||||
body = json.dumps(payload).encode("utf-8")
|
||||
req = request.Request(url, data=body, method="POST")
|
||||
req.add_header("Content-Type", "application/json")
|
||||
req.add_header("Authorization", f"Bearer {self.settings.home_assistant_auth_token}")
|
||||
|
||||
try:
|
||||
with request.urlopen(req, timeout=self.timeout_seconds) as response:
|
||||
status_code = response.getcode()
|
||||
except error.HTTPError as exc:
|
||||
logger.warning(
|
||||
"Home Assistant outbound %s failed with HTTP %s for %s",
|
||||
operation,
|
||||
exc.code,
|
||||
url,
|
||||
)
|
||||
raise HomeAssistantRequestError(
|
||||
f"Home Assistant outbound {operation} failed with HTTP {exc.code}"
|
||||
) from exc
|
||||
except error.URLError as exc:
|
||||
logger.warning("Home Assistant outbound %s failed for %s: %s", operation, url, exc)
|
||||
raise HomeAssistantRequestError(
|
||||
f"Home Assistant outbound {operation} failed to reach Home Assistant"
|
||||
) from exc
|
||||
|
||||
if status_code not in SUCCESS_STATUS_CODES:
|
||||
logger.warning(
|
||||
"Home Assistant outbound %s returned unexpected status %s for %s",
|
||||
operation,
|
||||
status_code,
|
||||
url,
|
||||
)
|
||||
raise HomeAssistantRequestError(
|
||||
f"Home Assistant outbound {operation} returned unexpected status {status_code}"
|
||||
)
|
||||
|
||||
def _build_url(self, path: str) -> str:
|
||||
base_url = self.settings.home_assistant_base_url.rstrip("/")
|
||||
quoted_path = parse.quote(path.lstrip("/"), safe="/")
|
||||
return f"{base_url}/{quoted_path}"
|
||||
|
||||
Reference in New Issue
Block a user