Shared backend + frontend for multi-customer EMS deployments. - 12 enterprise modules: quota, cost, charging, maintenance, analysis, etc. - 120+ API endpoints, 37 database tables - Customer config mechanism (CUSTOMER env var + YAML config) - Collectors: Modbus TCP, MQTT, HTTP API, Sungrow iSolarCloud - Frontend: React 19 + Ant Design + ECharts + Three.js - Infrastructure: Redis cache, rate limiting, aggregation engine Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
89 lines
3.0 KiB
Python
89 lines
3.0 KiB
Python
from pydantic_settings import BaseSettings
|
|
from functools import lru_cache
|
|
import os
|
|
|
|
import yaml
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
APP_NAME: str = "TianpuEMS"
|
|
DEBUG: bool = True
|
|
API_V1_PREFIX: str = "/api/v1"
|
|
|
|
# Customer configuration
|
|
CUSTOMER: str = "tianpu" # tianpu, zpark, etc.
|
|
CUSTOMER_DISPLAY_NAME: str = "" # Loaded from customer config
|
|
|
|
# Database: set DATABASE_URL in .env to override.
|
|
# Default: SQLite for local dev. Docker sets PostgreSQL via env var.
|
|
# Examples:
|
|
# SQLite: sqlite+aiosqlite:///./tianpu_ems.db
|
|
# PostgreSQL: postgresql+asyncpg://tianpu:tianpu2026@localhost:5432/tianpu_ems
|
|
DATABASE_URL: str = "sqlite+aiosqlite:///./tianpu_ems.db"
|
|
REDIS_URL: str = "redis://localhost:6379/0"
|
|
|
|
SECRET_KEY: str = "tianpu-ems-secret-key-change-in-production-2026"
|
|
ALGORITHM: str = "HS256"
|
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 480
|
|
|
|
CELERY_ENABLED: bool = False # Set True when Celery worker is running
|
|
USE_SIMULATOR: bool = True # True=simulator mode, False=real IoT collectors
|
|
|
|
# Infrastructure flags
|
|
TIMESCALE_ENABLED: bool = False
|
|
REDIS_ENABLED: bool = True
|
|
INGESTION_QUEUE_ENABLED: bool = False
|
|
AGGREGATION_ENABLED: bool = True
|
|
|
|
# SMTP Email settings
|
|
SMTP_HOST: str = ""
|
|
SMTP_PORT: int = 587
|
|
SMTP_USER: str = ""
|
|
SMTP_PASSWORD: str = ""
|
|
SMTP_FROM: str = "noreply@tianpu-ems.com"
|
|
SMTP_ENABLED: bool = False
|
|
|
|
# Platform URL for links in emails
|
|
PLATFORM_URL: str = "http://localhost:3000"
|
|
|
|
@property
|
|
def DATABASE_URL_SYNC(self) -> str:
|
|
"""Derive synchronous URL from async DATABASE_URL for Alembic."""
|
|
url = self.DATABASE_URL
|
|
return url.replace("+aiosqlite", "").replace("+asyncpg", "+psycopg2")
|
|
|
|
@property
|
|
def is_sqlite(self) -> bool:
|
|
return "sqlite" in self.DATABASE_URL
|
|
|
|
@property
|
|
def customer_config_path(self) -> str:
|
|
"""Search for customer config in multiple locations."""
|
|
backend_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
|
# Standalone: project_root/customers/{CUSTOMER}/
|
|
path1 = os.path.join(backend_dir, "..", "customers", self.CUSTOMER)
|
|
if os.path.isdir(path1):
|
|
return os.path.abspath(path1)
|
|
# Subtree: customer_project_root/customers/{CUSTOMER}/ (core is 2 levels up)
|
|
path2 = os.path.join(backend_dir, "..", "..", "customers", self.CUSTOMER)
|
|
if os.path.isdir(path2):
|
|
return os.path.abspath(path2)
|
|
return os.path.abspath(path1) # Default fallback
|
|
|
|
def load_customer_config(self) -> dict:
|
|
"""Load customer-specific config from customers/{CUSTOMER}/config.yaml"""
|
|
config_file = os.path.join(self.customer_config_path, "config.yaml")
|
|
if os.path.exists(config_file):
|
|
with open(config_file, 'r', encoding='utf-8') as f:
|
|
return yaml.safe_load(f) or {}
|
|
return {}
|
|
|
|
class Config:
|
|
env_file = ".env"
|
|
extra = "ignore"
|
|
|
|
|
|
@lru_cache
|
|
def get_settings() -> Settings:
|
|
return Settings()
|