From 60e7f08d7e58cfefd5f114e56ee1d5c0c558a9cc Mon Sep 17 00:00:00 2001 From: Du Wenbo Date: Mon, 6 Apr 2026 22:02:38 +0800 Subject: [PATCH] feat!: generic defaults, dashboard fallback, PV filter fix (v1.3.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace all hardcoded Tianpu/天普 defaults with generic "EMS Platform" - Add energy_today fallback: query raw energy_data when daily summary empty - Fix PV device filter to include sungrow_inverter device type - Update APP_NAME, CUSTOMER default, SECRET_KEY, SMTP, Celery, email templates BREAKING: CUSTOMER default changed from "tianpu" to "default" Existing deployments with CUSTOMER=tianpu in .env are unaffected. Co-Authored-By: Claude Opus 4.6 (1M context) --- VERSION | 2 +- VERSIONS.json | 4 +-- backend/alembic.ini | 2 +- .../versions/002_add_system_settings.py | 2 +- backend/app/api/v1/dashboard.py | 36 +++++++++++++++++-- backend/app/api/v1/settings.py | 2 +- backend/app/core/config.py | 14 ++++---- backend/app/main.py | 4 +-- backend/app/services/alarm_checker.py | 2 +- backend/app/services/report_generator.py | 4 +-- backend/app/services/report_scheduler.py | 4 +-- backend/app/services/simulator.py | 2 +- backend/app/services/weather_model.py | 4 +-- backend/app/tasks/celery_app.py | 2 +- backend/app/templates/alarm_email.html | 6 ++-- 15 files changed, 61 insertions(+), 29 deletions(-) diff --git a/VERSION b/VERSION index 26aaba0..f0bb29e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.0 +1.3.0 diff --git a/VERSIONS.json b/VERSIONS.json index 3d22449..f0741b2 100644 --- a/VERSIONS.json +++ b/VERSIONS.json @@ -1,6 +1,6 @@ { "project": "ems-core", - "project_version": "1.2.0", + "project_version": "1.3.0", "last_updated": "2026-04-06", - "notes": "Backend-only architecture, frontend removed to customer repos" + "notes": "Generic defaults, dashboard energy fallback, PV device type filter fix" } diff --git a/backend/alembic.ini b/backend/alembic.ini index 24f9877..2ac5a91 100644 --- a/backend/alembic.ini +++ b/backend/alembic.ini @@ -1,6 +1,6 @@ [alembic] script_location = alembic -sqlalchemy.url = postgresql://tianpu:tianpu2026@localhost:5432/tianpu_ems +sqlalchemy.url = postgresql://ems:ems2026@localhost:5432/ems [loggers] keys = root,sqlalchemy,alembic diff --git a/backend/alembic/versions/002_add_system_settings.py b/backend/alembic/versions/002_add_system_settings.py index 850e03a..25796a7 100644 --- a/backend/alembic/versions/002_add_system_settings.py +++ b/backend/alembic/versions/002_add_system_settings.py @@ -26,7 +26,7 @@ def upgrade() -> None: # Seed default settings op.execute(""" INSERT INTO system_settings (key, value, description) VALUES - ('platform_name', '天普零碳园区智慧能源管理平台', '平台名称'), + ('platform_name', 'Smart Energy Management Platform', '平台名称'), ('data_retention_days', '365', '数据保留天数'), ('alarm_auto_resolve_minutes', '30', '告警自动解除时间(分钟)'), ('simulator_interval_seconds', '15', '模拟器采集间隔(秒)'), diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py index c46fd64..0df1e3d 100644 --- a/backend/app/api/v1/dashboard.py +++ b/backend/app/api/v1/dashboard.py @@ -26,7 +26,7 @@ async def get_overview(db: AsyncSession = Depends(get_db), user: User = Depends( ) device_stats = {row[0]: row[1] for row in device_stats_q.all()} - # 今日能耗汇总 + # 今日能耗汇总 (from daily summary table) daily_q = await db.execute( select( EnergyDailySummary.energy_type, @@ -38,6 +38,35 @@ async def get_overview(db: AsyncSession = Depends(get_db), user: User = Depends( for row in daily_q.all(): energy_summary[row[0]] = {"consumption": row[1] or 0, "generation": row[2] or 0} + # Fallback: if daily summary is empty, compute from raw energy_data + if not energy_summary: + from sqlalchemy import distinct + fallback_q = await db.execute( + select( + func.sum(EnergyData.value), + ).where( + and_( + EnergyData.timestamp >= today_start, + EnergyData.data_type == "daily_energy", + ) + ).group_by(EnergyData.device_id).order_by(EnergyData.device_id) + ) + # Get the latest daily_energy per device (avoid double-counting) + latest_energy_q = await db.execute( + select( + EnergyData.device_id, + func.max(EnergyData.value).label("max_energy"), + ).where( + and_( + EnergyData.timestamp >= today_start, + EnergyData.data_type == "daily_energy", + ) + ).group_by(EnergyData.device_id) + ) + total_gen = sum(row[1] or 0 for row in latest_energy_q.all()) + if total_gen > 0: + energy_summary["electricity"] = {"consumption": 0, "generation": round(total_gen, 2)} + # 今日碳排放 carbon_q = await db.execute( select(func.sum(CarbonEmission.emission), func.sum(CarbonEmission.reduction)) @@ -134,7 +163,10 @@ async def get_load_curve( async def _get_pv_device_ids(db: AsyncSession): result = await db.execute( - select(Device.id).where(Device.device_type == "pv_inverter", Device.is_active == True) + select(Device.id).where( + Device.device_type.in_(["pv_inverter", "sungrow_inverter"]), + Device.is_active == True, + ) ) return [r[0] for r in result.fetchall()] diff --git a/backend/app/api/v1/settings.py b/backend/app/api/v1/settings.py index fcede7e..e2edd7c 100644 --- a/backend/app/api/v1/settings.py +++ b/backend/app/api/v1/settings.py @@ -12,7 +12,7 @@ router = APIRouter(prefix="/settings", tags=["系统设置"]) # Default settings — used when keys are missing from DB DEFAULTS: dict[str, str] = { - "platform_name": "天普零碳园区智慧能源管理平台", + "platform_name": "Smart Energy Management Platform", "data_retention_days": "365", "alarm_auto_resolve_minutes": "30", "simulator_interval_seconds": "15", diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 154309a..9435d37 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -6,23 +6,23 @@ import yaml class Settings(BaseSettings): - APP_NAME: str = "TianpuEMS" + APP_NAME: str = "EMS Platform" DEBUG: bool = True API_V1_PREFIX: str = "/api/v1" # Customer configuration - CUSTOMER: str = "tianpu" # tianpu, zpark, etc. + CUSTOMER: str = "default" # 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" + # SQLite: sqlite+aiosqlite:///./ems.db + # PostgreSQL: postgresql+asyncpg://ems:ems2026@localhost:5432/ems + DATABASE_URL: str = "sqlite+aiosqlite:///./ems.db" REDIS_URL: str = "redis://localhost:6379/0" - SECRET_KEY: str = "tianpu-ems-secret-key-change-in-production-2026" + SECRET_KEY: str = "ems-secret-key-change-in-production-2026" ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 480 @@ -40,7 +40,7 @@ class Settings(BaseSettings): SMTP_PORT: int = 587 SMTP_USER: str = "" SMTP_PASSWORD: str = "" - SMTP_FROM: str = "noreply@tianpu-ems.com" + SMTP_FROM: str = "noreply@ems-platform.com" SMTP_ENABLED: bool = False # Platform URL for links in emails diff --git a/backend/app/main.py b/backend/app/main.py index db6a6ab..411daf7 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -84,8 +84,8 @@ async def lifespan(app: FastAPI): app = FastAPI( - title=customer_config.get("platform_name", "天普零碳园区智慧能源管理平台"), - description=customer_config.get("platform_name_en", "Tianpu Zero-Carbon Park Smart Energy Management System"), + title=customer_config.get("platform_name", "Smart Energy Management Platform"), + description=customer_config.get("platform_name_en", "Smart Energy Management System"), version="1.0.0", lifespan=lifespan, ) diff --git a/backend/app/services/alarm_checker.py b/backend/app/services/alarm_checker.py index b66b3f7..43573f1 100644 --- a/backend/app/services/alarm_checker.py +++ b/backend/app/services/alarm_checker.py @@ -104,7 +104,7 @@ async def _send_alarm_email( platform_url=settings.PLATFORM_URL, ) - subject = f"[{severity_cfg['label']}] {event.title} - 天普EMS告警通知" + subject = f"[{severity_cfg['label']}] {event.title} - EMS Alarm Notification" asyncio.create_task(send_email(to=emails, subject=subject, body_html=body_html)) # Rate limit: don't create duplicate events for the same rule+device within this window diff --git a/backend/app/services/report_generator.py b/backend/app/services/report_generator.py index 55179fd..7e0f24c 100644 --- a/backend/app/services/report_generator.py +++ b/backend/app/services/report_generator.py @@ -1,5 +1,5 @@ """ -报表生成服务 - PDF/Excel report generation for Tianpu EMS. +报表生成服务 - PDF/Excel report generation for EMS Platform. """ import os import io @@ -18,7 +18,7 @@ from app.models.carbon import CarbonEmission REPORTS_DIR = Path(__file__).resolve().parent.parent.parent / "reports" REPORTS_DIR.mkdir(exist_ok=True) -PLATFORM_TITLE = "天普零碳园区智慧能源管理平台" +PLATFORM_TITLE = "Smart Energy Management Platform" ENERGY_TYPE_LABELS = { "electricity": "电力", diff --git a/backend/app/services/report_scheduler.py b/backend/app/services/report_scheduler.py index f286ace..77e42ab 100644 --- a/backend/app/services/report_scheduler.py +++ b/backend/app/services/report_scheduler.py @@ -103,10 +103,10 @@ async def _run_report_task(task_id: int): recipients = task.recipients or [] if isinstance(recipients, list) and recipients: report_name = task.name or template.name - subject = f"{report_name} - 天普EMS自动报表" + subject = f"{report_name} - EMS Auto Report" body_html = f"""
-

天普零碳园区智慧能源管理平台

+

Smart Energy Management Platform

您好,

系统已自动生成 {report_name},请查收附件。

diff --git a/backend/app/services/simulator.py b/backend/app/services/simulator.py index 472b6ea..9155570 100644 --- a/backend/app/services/simulator.py +++ b/backend/app/services/simulator.py @@ -1,4 +1,4 @@ -"""模拟数据生成器 - 为天普园区设备生成真实感的模拟数据 +"""模拟数据生成器 - 为园区设备生成真实感的模拟数据 Uses physics-based solar position, Beijing weather models, cloud transients, temperature derating, and realistic building load patterns to produce data diff --git a/backend/app/services/weather_model.py b/backend/app/services/weather_model.py index c7f6090..78b0e5e 100644 --- a/backend/app/services/weather_model.py +++ b/backend/app/services/weather_model.py @@ -3,7 +3,7 @@ Shared by both the real-time simulator and the backfill script. Deterministic when given a seed — call set_seed() for reproducible backfills. -Tianpu campus: 39.9N, 116.4E (Beijing / Daxing district) +Default campus: 39.9N, 116.4E (Beijing / Daxing district) """ import math @@ -87,7 +87,7 @@ def solar_altitude(dt: datetime) -> float: # Local solar time beijing_dt = dt + timedelta(hours=BEIJING_TZ_OFFSET) if dt.tzinfo else dt - # Standard meridian for UTC+8 is 120E; Tianpu is at 116.4E + # Standard meridian for UTC+8 is 120E; default campus is at 116.4E time_offset = _equation_of_time(doy) + 4 * (BEIJING_LON - 120.0) solar_hour = beijing_dt.hour + beijing_dt.minute / 60.0 + beijing_dt.second / 3600.0 solar_hour += time_offset / 60.0 diff --git a/backend/app/tasks/celery_app.py b/backend/app/tasks/celery_app.py index 0f5f0f2..6c232a3 100644 --- a/backend/app/tasks/celery_app.py +++ b/backend/app/tasks/celery_app.py @@ -4,7 +4,7 @@ from app.core.config import get_settings settings = get_settings() celery_app = Celery( - "tianpu_ems", + "ems_platform", broker=settings.REDIS_URL, backend=settings.REDIS_URL, ) diff --git a/backend/app/templates/alarm_email.html b/backend/app/templates/alarm_email.html index e2da3e0..1bfc055 100644 --- a/backend/app/templates/alarm_email.html +++ b/backend/app/templates/alarm_email.html @@ -13,8 +13,8 @@ -

TIANPU EMS
-
天普零碳园区智慧能源管理平台
+
EMS PLATFORM
+
Smart Energy Management Platform
@@ -85,7 +85,7 @@
此为系统自动发送,请勿回复。
- 天普零碳园区智慧能源管理平台 © 2026 + Smart Energy Management Platform © 2026