Add Home Assistant inbound gateway

This commit is contained in:
2026-04-20 10:42:35 +02:00
parent 151ad46275
commit e334df992f
13 changed files with 380 additions and 29 deletions
+49
View File
@@ -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)
+5 -5
View File
@@ -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)
+2
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
from pydantic import BaseModel, ConfigDict
class HomeAssistantPublishEnvelope(BaseModel):
target: str
action: str
content: str
model_config = ConfigDict(extra="forbid")
+35
View File
@@ -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)