Files
tianpu-ems/backend/app/main.py
Du Wenbo 02c4698b59 feat: multi-customer architecture + Z-Park support + Gitea migration scripts
Multi-customer config system:
- CUSTOMER env var selects customer (tianpu/zpark)
- customers/tianpu/config.yaml — Tianpu branding, collectors, features
- customers/zpark/config.yaml — Z-Park branding, Sungrow collector
- GET /api/v1/branding endpoint for customer-specific branding
- main.py loads customer config for app title, CORS, logging
- Collector manager filters by customer's enabled collectors

Z-Park (中关村医疗器械园) support:
- Sungrow iSolarCloud API collector (sungrow_collector.py)
- Z-Park device definitions (10 inverters, 8 combiner boxes, 22+ buildings)
- Z-Park TOU pricing config (Beijing 2026 rates)
- Z-Park seed script (seed_zpark.py)

Gitea migration scripts (Mac Studio → labmac3):
- 5 migration scripts + README in scripts/gitea-migration/
- Creates 3-repo structure: ems-core, tp-ems, zpark-ems

Version: v1.0.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 16:23:33 +08:00

136 lines
4.3 KiB
Python

import logging
import uuid
from contextlib import asynccontextmanager
from typing import Optional
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from app.api.router import api_router
from app.api.v1.websocket import start_broadcast_task, stop_broadcast_task
from app.core.config import get_settings
from app.core.cache import get_redis, close_redis
from app.services.simulator import DataSimulator
from app.services.report_scheduler import start_scheduler, stop_scheduler
from app.services.aggregation import start_aggregation_scheduler, stop_aggregation_scheduler
from app.collectors.manager import CollectorManager
from app.collectors.queue import IngestionWorker
settings = get_settings()
customer_config = settings.load_customer_config()
simulator = DataSimulator()
collector_manager: Optional[CollectorManager] = None
ingestion_worker: Optional[IngestionWorker] = None
logger = logging.getLogger("app")
@asynccontextmanager
async def lifespan(app: FastAPI):
global collector_manager, ingestion_worker
logger.info("Loading customer: %s (%s)", settings.CUSTOMER,
customer_config.get("customer_name", settings.CUSTOMER))
# Initialize Redis cache
if settings.REDIS_ENABLED:
redis = await get_redis()
if redis:
logger.info("Redis cache initialized")
# Start aggregation scheduler
if settings.AGGREGATION_ENABLED:
await start_aggregation_scheduler()
logger.info("Aggregation scheduler started")
# Start ingestion worker
if settings.INGESTION_QUEUE_ENABLED:
ingestion_worker = IngestionWorker()
await ingestion_worker.start()
logger.info("Ingestion worker started")
if settings.USE_SIMULATOR:
logger.info("Starting in SIMULATOR mode")
await simulator.start()
else:
logger.info("Starting in COLLECTOR mode (real IoT devices)")
collector_manager = CollectorManager()
await collector_manager.start()
start_broadcast_task()
await start_scheduler()
yield
await stop_scheduler()
stop_broadcast_task()
if settings.USE_SIMULATOR:
await simulator.stop()
else:
if collector_manager:
await collector_manager.stop()
collector_manager = None
# Stop ingestion worker
if ingestion_worker:
await ingestion_worker.stop()
ingestion_worker = None
# Stop aggregation scheduler
if settings.AGGREGATION_ENABLED:
await stop_aggregation_scheduler()
# Close Redis
if settings.REDIS_ENABLED:
await close_redis()
logger.info("Redis cache closed")
app = FastAPI(
title=customer_config.get("platform_name", "天普零碳园区智慧能源管理平台"),
description=customer_config.get("platform_name_en", "Tianpu Zero-Carbon Park Smart Energy Management System"),
version="1.0.0",
lifespan=lifespan,
)
_default_origins = ["http://localhost:3000", "http://localhost:5173", "http://127.0.0.1:3000", "http://127.0.0.1:5173"]
_customer_origins = customer_config.get("cors_origins", [])
_cors_origins = list(set(_default_origins + _customer_origins))
app.add_middleware(
CORSMiddleware,
allow_origins=_cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.middleware("http")
async def request_id_middleware(request: Request, call_next):
"""Add a unique X-Request-ID header to every response."""
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
request.state.request_id = request_id
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
"""Global exception handler for consistent error responses."""
request_id = getattr(request.state, "request_id", "unknown")
logger.error("Unhandled exception [request_id=%s]: %s", request_id, exc, exc_info=True)
return JSONResponse(
status_code=500,
content={
"detail": "Internal server error",
"request_id": request_id,
},
)
app.include_router(api_router)
@app.get("/health")
async def health():
return {"status": "ok", "app": settings.APP_NAME}