Squashed 'core/' content from commit 92ec910
git-subtree-dir: core git-subtree-split: 92ec910a132e379a3a6e442a75bcb07cac0f0010
This commit is contained in:
229
backend/app/services/weather_service.py
Normal file
229
backend/app/services/weather_service.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""气象数据融合服务 - 天气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": "气象配置更新成功"}
|
||||
Reference in New Issue
Block a user