Files
ems/backend/app/main.py
Dusan Vojacek 8b4af663d8 Initial commit
Made-with: Cursor
2026-03-20 13:27:44 +01:00

158 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""EMS FastAPI health, provozní režimy, PostgREST doplňky."""
from __future__ import annotations
import logging
import os
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from typing import Annotated
import asyncpg
import httpx
from app.deps import set_pg_pool
from app.routers.plan import router as plan_router
from fastapi import Depends, FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
def _dsn() -> str:
host = os.getenv("DB_HOST", "localhost")
port = os.getenv("DB_PORT", "5432")
name = os.getenv("DB_NAME", "ems")
user = os.getenv("DB_USER", "ems_user")
password = os.getenv("DB_PASSWORD", "")
return f"postgresql://{user}:{password}@{host}:{port}/{name}"
pool: asyncpg.Pool | None = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global pool
pool = await asyncpg.create_pool(_dsn(), min_size=1, max_size=5)
set_pg_pool(pool)
yield
set_pg_pool(None)
if pool:
await pool.close()
pool = None
app = FastAPI(title="EMS Platform", lifespan=lifespan)
app.include_router(plan_router, prefix="/api/v1")
app.add_middleware(
CORSMiddleware,
allow_origins=os.getenv("CORS_ORIGINS", "http://localhost:5173,http://127.0.0.1:5173").split(","),
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
async def get_pool() -> asyncpg.Pool:
if pool is None:
raise HTTPException(status_code=503, detail="Database pool not ready")
return pool
@app.get("/health")
async def health() -> dict[str, str]:
return {"status": "ok"}
class SetSiteModeBody(BaseModel):
mode: str = Field(..., min_length=1)
notes: str | None = None
valid_until: datetime | None = None
class SetSiteModeResponse(BaseModel):
success: bool
mode: str
activated_at: datetime
@app.post("/api/v1/sites/{site_id}/mode", response_model=SetSiteModeResponse)
async def set_site_mode(
site_id: int,
body: SetSiteModeBody,
db: Annotated[asyncpg.Pool, Depends(get_pool)],
) -> SetSiteModeResponse:
mode = body.mode.strip().upper()
allowed = {"AUTO", "SELF_SUSTAIN", "CHARGE_CHEAP", "PRESERVE", "MANUAL"}
if mode not in allowed:
raise HTTPException(status_code=400, detail=f"Unsupported mode: {body.mode}")
async with db.acquire() as conn:
site_ok = await conn.fetchval("SELECT EXISTS(SELECT 1 FROM ems.site WHERE id = $1)", site_id)
if not site_ok:
raise HTTPException(status_code=404, detail="Site not found")
try:
await conn.execute(
"SELECT ems.fn_set_mode($1, $2, $3, $4, $5)",
site_id,
mode,
"user:api",
body.valid_until,
body.notes,
)
except asyncpg.PostgresError as e:
logger.warning("fn_set_mode failed: %s", e)
raise HTTPException(status_code=400, detail=str(e)) from e
row = await conn.fetchrow(
"""
SELECT m.mode_code, m.activated_at, d.loxone_mode_value
FROM ems.site_operating_mode m
JOIN ems.operating_mode_def d ON d.code = m.mode_code
WHERE m.site_id = $1
""",
site_id,
)
if row is None:
raise HTTPException(status_code=500, detail="Mode row missing after set")
ep = await conn.fetchrow(
"""
SELECT host, port, protocol
FROM ems.site_endpoint
WHERE site_id = $1 AND endpoint_type = 'loxone_http' AND enabled = true
ORDER BY id
LIMIT 1
""",
site_id,
)
activated_at: datetime = row["activated_at"]
if activated_at.tzinfo is None:
activated_at = activated_at.replace(tzinfo=timezone.utc)
loxone_val: int | None = row["loxone_mode_value"]
if ep and loxone_val is not None:
proto = (ep["protocol"] or "http").lower()
if proto not in ("http", "https"):
proto = "http"
host = ep["host"]
port = int(ep["port"] or (443 if proto == "https" else 80))
base = f"{proto}://{host}:{port}"
url = f"{base}/dev/sps/io/EMS_Mode/{loxone_val}"
user = os.getenv("LOXONE_USER") or ""
password = os.getenv("LOXONE_PASSWORD") or ""
auth = (user, password) if user else None
try:
async with httpx.AsyncClient(timeout=10.0) as client:
r = await client.get(url, auth=auth)
r.raise_for_status()
except Exception as e:
logger.warning("Loxone EMS_Mode notify failed for site %s: %s", site_id, e)
return SetSiteModeResponse(success=True, mode=row["mode_code"], activated_at=activated_at)