Files
tp-ems/core/backend/app/services/weather_service.py

230 lines
7.6 KiB
Python

"""气象数据融合服务 - 天气API集成、模拟数据生成、缓存"""
import logging
import math
from datetime import datetime, timedelta, timezone
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, desc
from app.models.weather import WeatherData, WeatherConfig
from app.services.weather_model import (
outdoor_temperature, outdoor_humidity, solar_altitude,
get_cloud_factor, BEIJING_TZ_OFFSET, MONTHLY_AVG_TEMP,
)
logger = logging.getLogger("weather_service")
BJT = timezone(timedelta(hours=8))
def generate_mock_weather(dt: datetime) -> dict:
"""Generate mock weather data based on weather_model patterns."""
temp = outdoor_temperature(dt)
humidity = outdoor_humidity(dt)
# Solar radiation based on altitude
alt = solar_altitude(dt)
if alt > 0:
cloud = get_cloud_factor(dt)
# Clear-sky irradiance ~ 1000 * sin(altitude) * cloud_factor
solar_radiation = 1000 * math.sin(math.radians(alt)) * cloud * 0.85
solar_radiation = max(0, solar_radiation)
cloud_cover = (1 - cloud) * 100
else:
solar_radiation = 0
cloud_cover = 0
# Wind speed model - seasonal + random
beijing_dt = dt + timedelta(hours=BEIJING_TZ_OFFSET) if dt.tzinfo else dt
month = beijing_dt.month
# Spring is windier in Beijing
base_wind = {1: 2.5, 2: 3.0, 3: 4.0, 4: 4.5, 5: 3.5, 6: 2.5,
7: 2.0, 8: 2.0, 9: 2.5, 10: 3.0, 11: 3.0, 12: 2.5}.get(month, 2.5)
# Diurnal: windier during afternoon
hour = beijing_dt.hour
diurnal_wind = 0.5 * math.sin(math.pi * (hour - 6) / 12) if 6 <= hour <= 18 else -0.3
wind_speed = max(0.1, base_wind + diurnal_wind)
return {
"temperature": round(temp, 1),
"humidity": round(humidity, 1),
"solar_radiation": round(solar_radiation, 1),
"cloud_cover": round(max(0, min(100, cloud_cover)), 1),
"wind_speed": round(wind_speed, 1),
}
async def get_current_weather(db: AsyncSession) -> dict:
"""Get current weather - from cache or generate mock."""
now = datetime.now(timezone.utc)
# Try cache first (within last 15 minutes)
cache_cutoff = now - timedelta(minutes=15)
q = select(WeatherData).where(
and_(
WeatherData.data_type == "observation",
WeatherData.fetched_at >= cache_cutoff,
)
).order_by(desc(WeatherData.fetched_at)).limit(1)
result = await db.execute(q)
cached = result.scalar_one_or_none()
if cached:
return {
"timestamp": str(cached.timestamp),
"temperature": cached.temperature,
"humidity": cached.humidity,
"solar_radiation": cached.solar_radiation,
"cloud_cover": cached.cloud_cover,
"wind_speed": cached.wind_speed,
"source": cached.source,
}
# Generate mock data
mock = generate_mock_weather(now)
weather = WeatherData(
timestamp=now,
data_type="observation",
temperature=mock["temperature"],
humidity=mock["humidity"],
solar_radiation=mock["solar_radiation"],
cloud_cover=mock["cloud_cover"],
wind_speed=mock["wind_speed"],
source="mock",
)
db.add(weather)
return {
"timestamp": str(now),
**mock,
"source": "mock",
}
async def get_forecast(db: AsyncSession, hours: int = 72) -> list[dict]:
"""Get weather forecast for the next N hours."""
now = datetime.now(timezone.utc)
forecasts = []
for h in range(0, hours, 3): # 3-hour intervals
dt = now + timedelta(hours=h)
mock = generate_mock_weather(dt)
forecasts.append({
"timestamp": str(dt),
"hours_ahead": h,
**mock,
})
return forecasts
async def get_weather_history(
db: AsyncSession, start_date: datetime, end_date: datetime,
) -> list[dict]:
"""Get historical weather data."""
q = select(WeatherData).where(
and_(
WeatherData.timestamp >= start_date,
WeatherData.timestamp <= end_date,
)
).order_by(WeatherData.timestamp)
result = await db.execute(q)
records = result.scalars().all()
if records:
return [
{
"timestamp": str(r.timestamp),
"temperature": r.temperature,
"humidity": r.humidity,
"solar_radiation": r.solar_radiation,
"cloud_cover": r.cloud_cover,
"wind_speed": r.wind_speed,
"source": r.source,
}
for r in records
]
# Generate mock historical data if none cached
history = []
dt = start_date
while dt <= end_date:
mock = generate_mock_weather(dt)
history.append({"timestamp": str(dt), **mock, "source": "mock"})
dt += timedelta(hours=1)
return history
async def get_weather_impact(db: AsyncSession, days: int = 30) -> dict:
"""Analyze weather impact on energy consumption and PV generation."""
now = datetime.now(timezone.utc)
start = now - timedelta(days=days)
# Generate sample correlation data
temp_ranges = [
{"range": "< 0C", "min": -10, "max": 0, "avg_consumption": 850, "pv_generation": 180},
{"range": "0-10C", "min": 0, "max": 10, "avg_consumption": 720, "pv_generation": 220},
{"range": "10-20C", "min": 10, "max": 20, "avg_consumption": 550, "pv_generation": 310},
{"range": "20-30C", "min": 20, "max": 30, "avg_consumption": 680, "pv_generation": 380},
{"range": "> 30C", "min": 30, "max": 40, "avg_consumption": 780, "pv_generation": 350},
]
# Solar radiation vs PV output correlation
solar_correlation = []
for rad in range(0, 1001, 100):
# PV output roughly proportional to radiation with some losses
pv_output = rad * 0.33 * 0.85 # 330kWp * 85% efficiency
solar_correlation.append({
"solar_radiation": rad,
"pv_output_kw": round(pv_output, 1),
})
return {
"analysis_period_days": days,
"temperature_impact": temp_ranges,
"solar_correlation": solar_correlation,
"key_findings": [
"采暖季(11-3月)温度每降低1C,热泵能耗增加约3%",
"太阳辐射与光伏产出呈强正相关(R2=0.92)",
"多云天气光伏产出下降30-50%",
"春季大风天气对能耗影响较小,但对光伏面板散热有利",
],
}
async def get_weather_config(db: AsyncSession) -> dict:
"""Get weather API configuration."""
result = await db.execute(select(WeatherConfig).limit(1))
config = result.scalar_one_or_none()
if not config:
return {
"api_provider": "mock",
"location_lat": 39.9,
"location_lon": 116.4,
"fetch_interval_minutes": 30,
"is_enabled": True,
}
return {
"id": config.id,
"api_provider": config.api_provider,
"location_lat": config.location_lat,
"location_lon": config.location_lon,
"fetch_interval_minutes": config.fetch_interval_minutes,
"is_enabled": config.is_enabled,
}
async def update_weather_config(db: AsyncSession, data: dict) -> dict:
"""Update weather API configuration."""
result = await db.execute(select(WeatherConfig).limit(1))
config = result.scalar_one_or_none()
if not config:
config = WeatherConfig()
db.add(config)
for key in ("api_provider", "api_key", "location_lat", "location_lon",
"fetch_interval_minutes", "is_enabled"):
if key in data:
setattr(config, key, data[key])
return {"message": "气象配置更新成功"}