Add Home Assistant inbound gateway
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Request, status
|
||||
from fastapi.responses import PlainTextResponse, Response
|
||||
from pydantic import ValidationError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.dependencies import get_db
|
||||
from app.schemas.homeassistant import HomeAssistantPublishEnvelope
|
||||
from app.services.homeassistant_inbound import (
|
||||
UnsupportedHomeAssistantMessage,
|
||||
handle_homeassistant_message,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["homeassistant"])
|
||||
logger = logging.getLogger(__name__)
|
||||
BAD_REQUEST_MESSAGE = "bad request"
|
||||
INTERNAL_SERVER_ERROR_MESSAGE = "internal server error"
|
||||
|
||||
|
||||
@router.post("/homeassistant/publish")
|
||||
async def publish_from_homeassistant(
|
||||
request: Request, db: Session = Depends(get_db)
|
||||
) -> Response:
|
||||
try:
|
||||
raw_payload = await request.body()
|
||||
data = json.loads(raw_payload)
|
||||
envelope = HomeAssistantPublishEnvelope.model_validate(data)
|
||||
handle_homeassistant_message(db, envelope)
|
||||
except json.JSONDecodeError as exc:
|
||||
logger.warning("Rejected Home Assistant publish request due to invalid JSON: %s", exc)
|
||||
return PlainTextResponse(BAD_REQUEST_MESSAGE, status_code=status.HTTP_400_BAD_REQUEST)
|
||||
except ValidationError as exc:
|
||||
logger.warning(
|
||||
"Rejected Home Assistant publish request due to validation failure: %s", exc
|
||||
)
|
||||
return PlainTextResponse(BAD_REQUEST_MESSAGE, status_code=status.HTTP_400_BAD_REQUEST)
|
||||
except UnsupportedHomeAssistantMessage as exc:
|
||||
logger.warning("Home Assistant publish target/action unsupported: %s", exc)
|
||||
return PlainTextResponse(
|
||||
INTERNAL_SERVER_ERROR_MESSAGE,
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
except ValueError as exc:
|
||||
logger.warning("Rejected Home Assistant publish request due to invalid content: %s", exc)
|
||||
return PlainTextResponse(BAD_REQUEST_MESSAGE, status_code=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(status_code=status.HTTP_200_OK)
|
||||
@@ -1,7 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi import APIRouter, Depends, Request, status
|
||||
from fastapi.responses import PlainTextResponse, Response
|
||||
from pydantic import ValidationError
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -24,12 +24,12 @@ async def create_location_record(request: Request, db: Session = Depends(get_db)
|
||||
record_location(db, payload)
|
||||
except json.JSONDecodeError as exc:
|
||||
logger.warning("Rejected location request due to invalid JSON: %s", exc)
|
||||
return PlainTextResponse(BAD_REQUEST_MESSAGE, status_code=400)
|
||||
return PlainTextResponse(BAD_REQUEST_MESSAGE, status_code=status.HTTP_400_BAD_REQUEST)
|
||||
except ValidationError as exc:
|
||||
logger.warning("Rejected location request due to payload validation failure: %s", exc)
|
||||
return PlainTextResponse(BAD_REQUEST_MESSAGE, status_code=400)
|
||||
return PlainTextResponse(BAD_REQUEST_MESSAGE, status_code=status.HTTP_400_BAD_REQUEST)
|
||||
except ValueError as exc:
|
||||
logger.warning("Rejected location request due to invalid numeric input: %s", exc)
|
||||
return PlainTextResponse(BAD_REQUEST_MESSAGE, status_code=400)
|
||||
return PlainTextResponse(BAD_REQUEST_MESSAGE, status_code=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(status_code=200)
|
||||
return Response(status_code=status.HTTP_200_OK)
|
||||
|
||||
@@ -6,6 +6,7 @@ from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from app import models # noqa: F401
|
||||
from app.api.routes import pages, status
|
||||
from app.api.routes.homeassistant import router as homeassistant_router
|
||||
from app.api.routes.location import router as location_router
|
||||
from app.config import get_settings
|
||||
from scripts.location_db_adopt import LocationDatabaseAdoptionError, validate_location_runtime_db
|
||||
@@ -54,6 +55,7 @@ def create_app() -> FastAPI:
|
||||
|
||||
app.include_router(status.router)
|
||||
app.include_router(pages.router)
|
||||
app.include_router(homeassistant_router)
|
||||
app.include_router(location_router)
|
||||
return app
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class HomeAssistantPublishEnvelope(BaseModel):
|
||||
target: str
|
||||
action: str
|
||||
content: str
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.schemas.homeassistant import HomeAssistantPublishEnvelope
|
||||
from app.schemas.location import LocationRecordRequest
|
||||
from app.services.location import record_location
|
||||
|
||||
|
||||
class UnsupportedHomeAssistantMessage(RuntimeError):
|
||||
"""Raised when the inbound gateway receives a target/action that is not supported yet."""
|
||||
|
||||
|
||||
def handle_homeassistant_message(
|
||||
session: Session, envelope: HomeAssistantPublishEnvelope
|
||||
) -> None:
|
||||
if envelope.target == "location_recorder":
|
||||
_handle_location_message(session, envelope)
|
||||
return
|
||||
|
||||
raise UnsupportedHomeAssistantMessage(
|
||||
f"Unsupported Home Assistant target/action: {envelope.target}/{envelope.action}"
|
||||
)
|
||||
|
||||
|
||||
def _handle_location_message(session: Session, envelope: HomeAssistantPublishEnvelope) -> None:
|
||||
if envelope.action != "record":
|
||||
raise UnsupportedHomeAssistantMessage(
|
||||
f"Unsupported Home Assistant target/action: {envelope.target}/{envelope.action}"
|
||||
)
|
||||
|
||||
content = json.loads(envelope.content.replace("'", '"'))
|
||||
payload = LocationRecordRequest.model_validate(content)
|
||||
record_location(session, payload)
|
||||
Reference in New Issue
Block a user