Initial commit: Tianpu Zero-Carbon EMS Platform
Full-stack energy management system for Tianpu Daxing campus. - Frontend: React 19 + TypeScript + Ant Design + ECharts - Backend: FastAPI + SQLAlchemy + PostgreSQL/TimescaleDB - Features: PV monitoring, heat pump management, carbon tracking, alarms, reports Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
146
backend/app/api/v1/alarms.py
Normal file
146
backend/app/api/v1/alarms.py
Normal file
@@ -0,0 +1,146 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, and_
|
||||
from datetime import datetime, timezone
|
||||
from pydantic import BaseModel
|
||||
from app.core.database import get_db
|
||||
from app.core.deps import get_current_user, require_roles
|
||||
from app.models.alarm import AlarmRule, AlarmEvent
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/alarms", tags=["告警管理"])
|
||||
|
||||
|
||||
class AlarmRuleCreate(BaseModel):
|
||||
name: str
|
||||
device_id: int | None = None
|
||||
device_type: str | None = None
|
||||
data_type: str
|
||||
condition: str
|
||||
threshold: float | None = None
|
||||
threshold_high: float | None = None
|
||||
threshold_low: float | None = None
|
||||
duration: int = 0
|
||||
severity: str = "warning"
|
||||
notify_channels: list[str] | None = None
|
||||
notify_targets: list[str] | None = None
|
||||
silence_start: str | None = None
|
||||
silence_end: str | None = None
|
||||
|
||||
|
||||
@router.get("/rules")
|
||||
async def list_rules(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
result = await db.execute(select(AlarmRule).order_by(AlarmRule.id.desc()))
|
||||
return [_rule_to_dict(r) for r in result.scalars().all()]
|
||||
|
||||
|
||||
@router.post("/rules")
|
||||
async def create_rule(data: AlarmRuleCreate, db: AsyncSession = Depends(get_db), user: User = Depends(require_roles("admin", "energy_manager"))):
|
||||
rule = AlarmRule(**data.model_dump(), created_by=user.id)
|
||||
db.add(rule)
|
||||
await db.flush()
|
||||
return _rule_to_dict(rule)
|
||||
|
||||
|
||||
@router.put("/rules/{rule_id}")
|
||||
async def update_rule(rule_id: int, data: AlarmRuleCreate, db: AsyncSession = Depends(get_db), user: User = Depends(require_roles("admin", "energy_manager"))):
|
||||
result = await db.execute(select(AlarmRule).where(AlarmRule.id == rule_id))
|
||||
rule = result.scalar_one_or_none()
|
||||
if not rule:
|
||||
raise HTTPException(status_code=404, detail="规则不存在")
|
||||
for k, v in data.model_dump(exclude_unset=True).items():
|
||||
setattr(rule, k, v)
|
||||
return _rule_to_dict(rule)
|
||||
|
||||
|
||||
@router.delete("/rules/{rule_id}")
|
||||
async def delete_rule(rule_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(require_roles("admin", "energy_manager"))):
|
||||
result = await db.execute(select(AlarmRule).where(AlarmRule.id == rule_id))
|
||||
rule = result.scalar_one_or_none()
|
||||
if not rule:
|
||||
raise HTTPException(status_code=404, detail="规则不存在")
|
||||
rule.is_active = False
|
||||
return {"message": "已删除"}
|
||||
|
||||
|
||||
@router.get("/events")
|
||||
async def list_events(
|
||||
status: str | None = None,
|
||||
severity: str | None = None,
|
||||
device_id: int | None = None,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
query = select(AlarmEvent)
|
||||
if status:
|
||||
query = query.where(AlarmEvent.status == status)
|
||||
if severity:
|
||||
query = query.where(AlarmEvent.severity == severity)
|
||||
if device_id:
|
||||
query = query.where(AlarmEvent.device_id == device_id)
|
||||
|
||||
count_q = select(func.count()).select_from(query.subquery())
|
||||
total = (await db.execute(count_q)).scalar()
|
||||
|
||||
query = query.order_by(AlarmEvent.triggered_at.desc()).offset((page - 1) * page_size).limit(page_size)
|
||||
result = await db.execute(query)
|
||||
return {
|
||||
"total": total,
|
||||
"items": [{
|
||||
"id": e.id, "rule_id": e.rule_id, "device_id": e.device_id, "severity": e.severity,
|
||||
"title": e.title, "description": e.description, "value": e.value, "threshold": e.threshold,
|
||||
"status": e.status, "triggered_at": str(e.triggered_at),
|
||||
"acknowledged_at": str(e.acknowledged_at) if e.acknowledged_at else None,
|
||||
"resolved_at": str(e.resolved_at) if e.resolved_at else None,
|
||||
} for e in result.scalars().all()]
|
||||
}
|
||||
|
||||
|
||||
@router.post("/events/{event_id}/acknowledge")
|
||||
async def acknowledge_event(event_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
result = await db.execute(select(AlarmEvent).where(AlarmEvent.id == event_id))
|
||||
event = result.scalar_one_or_none()
|
||||
if not event:
|
||||
raise HTTPException(status_code=404, detail="告警不存在")
|
||||
event.status = "acknowledged"
|
||||
event.acknowledged_by = user.id
|
||||
event.acknowledged_at = datetime.now(timezone.utc)
|
||||
return {"message": "已确认"}
|
||||
|
||||
|
||||
@router.post("/events/{event_id}/resolve")
|
||||
async def resolve_event(event_id: int, note: str = "", db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
result = await db.execute(select(AlarmEvent).where(AlarmEvent.id == event_id))
|
||||
event = result.scalar_one_or_none()
|
||||
if not event:
|
||||
raise HTTPException(status_code=404, detail="告警不存在")
|
||||
event.status = "resolved"
|
||||
event.resolved_at = datetime.now(timezone.utc)
|
||||
event.resolve_note = note
|
||||
return {"message": "已解决"}
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def alarm_stats(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
result = await db.execute(
|
||||
select(AlarmEvent.severity, AlarmEvent.status, func.count(AlarmEvent.id))
|
||||
.group_by(AlarmEvent.severity, AlarmEvent.status)
|
||||
)
|
||||
stats = {}
|
||||
for severity, status, count in result.all():
|
||||
if severity not in stats:
|
||||
stats[severity] = {}
|
||||
stats[severity][status] = count
|
||||
return stats
|
||||
|
||||
|
||||
def _rule_to_dict(r: AlarmRule) -> dict:
|
||||
return {
|
||||
"id": r.id, "name": r.name, "device_id": r.device_id, "device_type": r.device_type,
|
||||
"data_type": r.data_type, "condition": r.condition, "threshold": r.threshold,
|
||||
"threshold_high": r.threshold_high, "threshold_low": r.threshold_low,
|
||||
"duration": r.duration, "severity": r.severity, "is_active": r.is_active,
|
||||
"notify_channels": r.notify_channels, "silence_start": r.silence_start, "silence_end": r.silence_end,
|
||||
}
|
||||
Reference in New Issue
Block a user