2026-04-19 20:19:58 +02:00
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from fastapi import FastAPI
|
|
|
|
|
from fastapi.staticfiles import StaticFiles
|
2026-04-29 11:45:49 +02:00
|
|
|
from apscheduler.schedulers.background import BackgroundScheduler
|
|
|
|
|
from apscheduler.triggers.interval import IntervalTrigger
|
2026-04-20 15:16:47 +02:00
|
|
|
from sqlalchemy.orm import Session
|
2026-04-19 20:19:58 +02:00
|
|
|
|
2026-04-19 21:39:23 +02:00
|
|
|
from app import models # noqa: F401
|
2026-04-20 15:16:47 +02:00
|
|
|
from app.api.routes.auth import router as auth_router
|
2026-04-19 20:19:58 +02:00
|
|
|
from app.api.routes import pages, status
|
2026-04-20 15:16:47 +02:00
|
|
|
import app.auth_db as auth_db
|
2026-04-20 10:42:35 +02:00
|
|
|
from app.api.routes.homeassistant import router as homeassistant_router
|
2026-04-19 21:39:23 +02:00
|
|
|
from app.api.routes.location import router as location_router
|
2026-04-20 11:48:48 +02:00
|
|
|
from app.api.routes.poo import router as poo_router
|
2026-04-29 11:45:49 +02:00
|
|
|
from app.api.routes.public_ip import router as public_ip_router
|
2026-04-20 17:06:03 +02:00
|
|
|
from app.api.routes.ticktick import router as ticktick_router
|
2026-04-19 20:19:58 +02:00
|
|
|
from app.config import get_settings
|
2026-04-20 15:16:47 +02:00
|
|
|
from app.services.auth import AuthBootstrapError, initialize_auth_schema
|
2026-04-20 20:40:04 +02:00
|
|
|
from app.services.config_page import seed_missing_config_from_bootstrap, sync_app_hostname_from_bootstrap
|
2026-04-29 11:45:49 +02:00
|
|
|
from app.services.public_ip import check_public_ipv4
|
2026-04-20 15:16:47 +02:00
|
|
|
from scripts.app_db_adopt import AppDatabaseAdoptionError, validate_app_runtime_db
|
2026-04-19 23:02:43 +02:00
|
|
|
from scripts.location_db_adopt import LocationDatabaseAdoptionError, validate_location_runtime_db
|
2026-04-20 11:48:48 +02:00
|
|
|
from scripts.poo_db_adopt import PooDatabaseAdoptionError, validate_poo_runtime_db
|
2026-04-19 23:02:43 +02:00
|
|
|
|
|
|
|
|
|
2026-04-29 11:45:49 +02:00
|
|
|
def _run_scheduled_public_ip_check() -> None:
|
|
|
|
|
session_local = auth_db.get_auth_session_local()
|
|
|
|
|
session: Session = session_local()
|
|
|
|
|
try:
|
|
|
|
|
check_public_ipv4(session)
|
|
|
|
|
finally:
|
|
|
|
|
session.close()
|
|
|
|
|
|
|
|
|
|
|
2026-04-20 15:16:47 +02:00
|
|
|
def ensure_auth_db_ready() -> None:
|
|
|
|
|
session_local = auth_db.get_auth_session_local()
|
|
|
|
|
session: Session = session_local()
|
|
|
|
|
try:
|
|
|
|
|
validate_app_runtime_db(get_settings().app_database_url)
|
|
|
|
|
initialize_auth_schema(session, get_settings())
|
2026-04-20 15:56:10 +02:00
|
|
|
seed_missing_config_from_bootstrap(session, get_settings())
|
2026-04-20 20:40:04 +02:00
|
|
|
sync_app_hostname_from_bootstrap(session, get_settings())
|
2026-04-20 15:16:47 +02:00
|
|
|
except AppDatabaseAdoptionError as exc:
|
|
|
|
|
raise RuntimeError(str(exc)) from exc
|
|
|
|
|
except AuthBootstrapError as exc:
|
|
|
|
|
raise RuntimeError(str(exc)) from exc
|
|
|
|
|
finally:
|
|
|
|
|
session.close()
|
|
|
|
|
|
|
|
|
|
|
2026-04-19 23:02:43 +02:00
|
|
|
def ensure_location_db_ready() -> None:
|
|
|
|
|
settings = get_settings()
|
|
|
|
|
if settings.location_sqlite_path is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
validate_location_runtime_db(settings.location_database_url)
|
|
|
|
|
except LocationDatabaseAdoptionError as exc:
|
|
|
|
|
raise RuntimeError(str(exc)) from exc
|
2026-04-19 20:19:58 +02:00
|
|
|
|
|
|
|
|
|
2026-04-20 11:48:48 +02:00
|
|
|
def ensure_poo_db_ready() -> None:
|
|
|
|
|
settings = get_settings()
|
|
|
|
|
if settings.poo_sqlite_path is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
validate_poo_runtime_db(settings.poo_database_url)
|
|
|
|
|
except PooDatabaseAdoptionError as exc:
|
|
|
|
|
raise RuntimeError(str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
2026-04-19 20:19:58 +02:00
|
|
|
def ensure_runtime_dirs() -> None:
|
|
|
|
|
settings = get_settings()
|
2026-04-20 15:16:47 +02:00
|
|
|
for path in (settings.app_sqlite_path, settings.location_sqlite_path, settings.poo_sqlite_path):
|
2026-04-19 21:39:23 +02:00
|
|
|
if path is not None:
|
|
|
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
2026-04-19 20:19:58 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
|
async def lifespan(_: FastAPI):
|
|
|
|
|
ensure_runtime_dirs()
|
2026-04-20 15:16:47 +02:00
|
|
|
ensure_auth_db_ready()
|
2026-04-19 23:02:43 +02:00
|
|
|
ensure_location_db_ready()
|
2026-04-20 11:48:48 +02:00
|
|
|
ensure_poo_db_ready()
|
2026-04-29 11:45:49 +02:00
|
|
|
scheduler = BackgroundScheduler(timezone="UTC")
|
|
|
|
|
scheduler.add_job(
|
|
|
|
|
_run_scheduled_public_ip_check,
|
|
|
|
|
trigger=IntervalTrigger(hours=4),
|
|
|
|
|
id="public-ip-check",
|
|
|
|
|
replace_existing=True,
|
|
|
|
|
max_instances=1,
|
|
|
|
|
coalesce=True,
|
|
|
|
|
)
|
|
|
|
|
scheduler.start()
|
2026-04-19 20:19:58 +02:00
|
|
|
yield
|
2026-04-29 11:45:49 +02:00
|
|
|
scheduler.shutdown(wait=False)
|
2026-04-19 20:19:58 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_app() -> FastAPI:
|
|
|
|
|
settings = get_settings()
|
|
|
|
|
app = FastAPI(
|
|
|
|
|
title=settings.app_name,
|
|
|
|
|
debug=settings.app_debug,
|
|
|
|
|
version="0.1.0",
|
|
|
|
|
lifespan=lifespan,
|
|
|
|
|
description=(
|
2026-04-20 20:40:04 +02:00
|
|
|
"Home automation backend with auth, runtime config, Home Assistant "
|
|
|
|
|
"integrations, TickTick integration, and SQLite-backed recorders."
|
2026-04-19 20:19:58 +02:00
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
static_dir = Path(__file__).parent / "static"
|
|
|
|
|
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
|
|
|
|
|
|
|
|
|
app.include_router(status.router)
|
2026-04-20 15:16:47 +02:00
|
|
|
app.include_router(auth_router)
|
2026-04-19 20:19:58 +02:00
|
|
|
app.include_router(pages.router)
|
2026-04-20 10:42:35 +02:00
|
|
|
app.include_router(homeassistant_router)
|
2026-04-19 21:39:23 +02:00
|
|
|
app.include_router(location_router)
|
2026-04-20 11:48:48 +02:00
|
|
|
app.include_router(poo_router)
|
2026-04-29 11:45:49 +02:00
|
|
|
app.include_router(public_ip_router)
|
2026-04-20 17:06:03 +02:00
|
|
|
app.include_router(ticktick_router)
|
2026-04-19 20:19:58 +02:00
|
|
|
return app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app = create_app()
|