216 lines
6.0 KiB
Python
216 lines
6.0 KiB
Python
"""UI main entry point."""
|
|
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.responses import HTMLResponse, JSONResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.templating import Jinja2Templates
|
|
|
|
from apps.ui.api_client import fetch_devices, fetch_layout
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Read configuration from environment variables
|
|
API_BASE = os.getenv("API_BASE", "http://localhost:8001")
|
|
BASE_PATH = os.getenv("BASE_PATH", "") # e.g., "/ui" for reverse proxy
|
|
|
|
print(f"UI using API_BASE: {API_BASE}")
|
|
print(f"UI using BASE_PATH: {BASE_PATH}")
|
|
|
|
def api_url(path: str) -> str:
|
|
"""Helper function to construct API URLs.
|
|
|
|
Args:
|
|
path: API path (e.g., "/devices")
|
|
|
|
Returns:
|
|
Full API URL
|
|
"""
|
|
return f"{API_BASE}{path}"
|
|
|
|
# Initialize FastAPI app with optional root_path for reverse proxy
|
|
app = FastAPI(
|
|
title="Home Automation UI",
|
|
description="User interface for home automation system",
|
|
version="0.1.0",
|
|
root_path=BASE_PATH
|
|
)
|
|
|
|
# Setup Jinja2 templates
|
|
templates_dir = Path(__file__).parent / "templates"
|
|
templates = Jinja2Templates(directory=str(templates_dir))
|
|
|
|
# Setup static files
|
|
static_dir = Path(__file__).parent / "static"
|
|
static_dir.mkdir(exist_ok=True)
|
|
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
|
|
|
|
|
@app.get("/health")
|
|
async def health() -> JSONResponse:
|
|
"""Health check endpoint for Kubernetes/Docker.
|
|
|
|
Returns:
|
|
JSONResponse: Health status
|
|
"""
|
|
return JSONResponse({
|
|
"status": "ok",
|
|
"service": "ui",
|
|
"api_base": API_BASE,
|
|
"base_path": BASE_PATH
|
|
})
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
async def index(request: Request) -> HTMLResponse:
|
|
"""Redirect to dashboard.
|
|
|
|
Args:
|
|
request: The FastAPI request object
|
|
|
|
Returns:
|
|
HTMLResponse: Rendered dashboard
|
|
"""
|
|
return await rooms(request)
|
|
|
|
|
|
@app.get("/rooms", response_class=HTMLResponse)
|
|
async def rooms(request: Request) -> HTMLResponse:
|
|
"""Render the rooms overview page.
|
|
|
|
Args:
|
|
request: The FastAPI request object
|
|
|
|
Returns:
|
|
HTMLResponse: Rendered rooms template
|
|
"""
|
|
return templates.TemplateResponse("rooms.html", {
|
|
"request": request,
|
|
"api_base": API_BASE
|
|
})
|
|
|
|
|
|
@app.get("/room/{room_name}", response_class=HTMLResponse)
|
|
async def room_detail(request: Request, room_name: str) -> HTMLResponse:
|
|
"""Render the room detail page with devices.
|
|
|
|
Args:
|
|
request: The FastAPI request object
|
|
room_name: Name of the room to display
|
|
|
|
Returns:
|
|
HTMLResponse: Rendered room template
|
|
"""
|
|
return templates.TemplateResponse("room.html", {
|
|
"request": request,
|
|
"api_base": API_BASE,
|
|
"room_name": room_name
|
|
})
|
|
|
|
|
|
@app.get("/device/{device_id}", response_class=HTMLResponse)
|
|
async def device_detail(request: Request, device_id: str) -> HTMLResponse:
|
|
"""Render the device detail page with controls.
|
|
|
|
Args:
|
|
request: The FastAPI request object
|
|
device_id: ID of the device to display
|
|
|
|
Returns:
|
|
HTMLResponse: Rendered device template
|
|
"""
|
|
return templates.TemplateResponse("device.html", {
|
|
"request": request,
|
|
"api_base": API_BASE,
|
|
"device_id": device_id
|
|
})
|
|
|
|
|
|
@app.get("/dashboard", response_class=HTMLResponse)
|
|
async def dashboard(request: Request) -> HTMLResponse:
|
|
"""Render the dashboard with rooms and devices.
|
|
|
|
Args:
|
|
request: The FastAPI request object
|
|
|
|
Returns:
|
|
HTMLResponse: Rendered dashboard template
|
|
"""
|
|
try:
|
|
# Load layout from API (use configured API_BASE)
|
|
layout_data = fetch_layout(API_BASE)
|
|
|
|
# Fetch devices from API (now includes features)
|
|
api_devices = fetch_devices(API_BASE)
|
|
|
|
# Create device lookup by device_id
|
|
device_map = {d["device_id"]: d for d in api_devices}
|
|
|
|
# Build rooms with merged device data
|
|
rooms = []
|
|
for room in layout_data.get("rooms", []):
|
|
devices = []
|
|
for tile in room.get("devices", []):
|
|
# Merge tile data with API device data
|
|
device_data = {
|
|
"device_id": tile["device_id"],
|
|
"title": tile["title"],
|
|
"icon": tile["icon"],
|
|
"rank": tile["rank"],
|
|
}
|
|
|
|
# Add type, name, and features from API if available
|
|
if tile["device_id"] in device_map:
|
|
api_device = device_map[tile["device_id"]]
|
|
device_data["type"] = api_device.get("type")
|
|
device_data["name"] = api_device.get("name")
|
|
device_data["features"] = api_device.get("features", {})
|
|
else:
|
|
device_data["features"] = {}
|
|
|
|
devices.append(device_data)
|
|
|
|
# Sort devices by rank (ascending)
|
|
devices.sort(key=lambda d: d["rank"])
|
|
|
|
rooms.append({
|
|
"name": room["name"],
|
|
"devices": devices
|
|
})
|
|
|
|
logger.info(f"Rendering dashboard with {len(rooms)} rooms")
|
|
|
|
return templates.TemplateResponse("dashboard.html", {
|
|
"request": request,
|
|
"rooms": rooms,
|
|
"api_base": API_BASE # Pass API_BASE to template
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error rendering dashboard: {e}", exc_info=True)
|
|
# Fallback to empty dashboard
|
|
return templates.TemplateResponse("dashboard.html", {
|
|
"request": request,
|
|
"rooms": [],
|
|
"api_base": API_BASE # Pass API_BASE even on error
|
|
})
|
|
|
|
|
|
def main() -> None:
|
|
"""Run the UI application with uvicorn."""
|
|
import uvicorn
|
|
|
|
uvicorn.run(
|
|
"apps.ui.main:app",
|
|
host="0.0.0.0",
|
|
port=8002,
|
|
reload=True
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|