"""气象数据融合服务 - 天气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": "气象配置更新成功"}