| | import math |
| | import requests |
| | import matplotlib.pyplot as plt |
| | import seaborn as sns |
| | import tempfile |
| | import os |
| | from config import NASA_FIRMS_MAP_KEY |
| | from datetime import datetime, timedelta |
| | from smolagents import tool |
| | from fpdf import FPDF |
| |
|
| |
|
| | @tool |
| | def get_coordinates(city: str) -> dict: |
| | """Get latitude and longitude of a city using OpenStreetMap Nominatim API. |
| | |
| | Args: |
| | city: Name of the city to get coordinates for |
| | |
| | Returns: |
| | Dict with city name, latitude, longitude, or error message |
| | """ |
| | url = "https://nominatim.openstreetmap.org/search" |
| | params = {"q": city, "format": "json", "limit": 1} |
| | headers = {"User-Agent": "ClimateRiskTool/1.0"} |
| | try: |
| | response = requests.get(url, params=params, headers=headers, timeout=10) |
| | data = response.json() |
| | if not data: |
| | return {"error": f"City '{city}' not found"} |
| | return { |
| | "city": city, |
| | "latitude": float(data[0]["lat"]), |
| | "longitude": float(data[0]["lon"]), |
| | } |
| | except Exception as e: |
| | return {"error": str(e)} |
| |
|
| |
|
| | @tool |
| | def get_weather_forecast(lat: float, lon: float) -> dict: |
| | """Get weather forecast data for risk analysis. |
| | |
| | Args: |
| | lat: Latitude coordinate |
| | lon: Longitude coordinate |
| | |
| | Returns: |
| | Dict with weather forecast data or error message |
| | """ |
| | url = "https://api.open-meteo.com/v1/forecast" |
| | params = { |
| | "latitude": lat, |
| | "longitude": lon, |
| | "daily": [ |
| | "temperature_2m_max", |
| | "temperature_2m_min", |
| | "precipitation_sum", |
| | "wind_speed_10m_max", |
| | "wind_gusts_10m_max", |
| | "relative_humidity_2m_min", |
| | ], |
| | "forecast_days": 7, |
| | "timezone": "auto", |
| | } |
| | try: |
| | response = requests.get(url, params=params, timeout=10) |
| | return response.json() |
| | except Exception as e: |
| | return {"error": str(e)} |
| |
|
| |
|
| | @tool |
| | def get_flood_data(lat: float, lon: float) -> dict: |
| | """Get flood forecast data. |
| | |
| | Args: |
| | lat: Latitude coordinate |
| | lon: Longitude coordinate |
| | |
| | Returns: |
| | Dict with flood forecast data or error message |
| | """ |
| | url = "https://flood-api.open-meteo.com/v1/flood" |
| | params = { |
| | "latitude": lat, |
| | "longitude": lon, |
| | "daily": ["river_discharge", "river_discharge_mean", "river_discharge_max"], |
| | "forecast_days": 7, |
| | } |
| | try: |
| | response = requests.get(url, params=params, timeout=10) |
| | return response.json() |
| | except Exception as e: |
| | return {"error": str(e)} |
| |
|
| |
|
| | @tool |
| | def get_earthquake_data( |
| | lat: float, lon: float, radius_km: float = 100, days: int = 30 |
| | ) -> dict: |
| | """Get raw earthquake data from USGS. |
| | |
| | Args: |
| | lat: Latitude coordinate |
| | lon: Longitude coordinate |
| | radius_km: Search radius in kilometers (default 100km) |
| | days: Number of days to look back (default 30 days) |
| | |
| | Returns: |
| | Dict with raw earthquake data from USGS |
| | """ |
| | url = "https://earthquake.usgs.gov/fdsnws/event/1/query" |
| |
|
| | end_date = datetime.now() |
| | start_date = end_date - timedelta(days=days) |
| |
|
| | params = { |
| | "format": "geojson", |
| | "starttime": start_date.strftime("%Y-%m-%d"), |
| | "endtime": end_date.strftime("%Y-%m-%d"), |
| | "latitude": lat, |
| | "longitude": lon, |
| | "maxradiuskm": radius_km, |
| | "minmagnitude": 1.0, |
| | "orderby": "time-desc", |
| | } |
| |
|
| | try: |
| | response = requests.get(url, params=params, timeout=15) |
| | response.raise_for_status() |
| | data = response.json() |
| |
|
| | earthquakes = [] |
| | for feature in data.get("features", []): |
| | props = feature["properties"] |
| | coords = feature["geometry"]["coordinates"] |
| |
|
| | earthquakes.append( |
| | { |
| | "magnitude": props.get("mag"), |
| | "place": props.get("place"), |
| | "time": props.get("time"), |
| | "depth": coords[2] if len(coords) > 2 else None, |
| | "latitude": coords[1], |
| | "longitude": coords[0], |
| | "alert": props.get("alert"), |
| | "significance": props.get("sig"), |
| | "event_type": props.get("type"), |
| | "title": props.get("title"), |
| | } |
| | ) |
| |
|
| | return { |
| | "earthquakes": earthquakes, |
| | "query_location": { |
| | "lat": lat, |
| | "lon": lon, |
| | "radius_km": radius_km, |
| | "days": days, |
| | }, |
| | "data_source": "USGS", |
| | } |
| |
|
| | except Exception as e: |
| | return {"error": str(e)} |
| |
|
| |
|
| | @tool |
| | def get_nasa_fire_data( |
| | lat: float, lon: float, radius_km: float = 50, days: int = 2 |
| | ) -> dict: |
| | """Get raw wildfire detection data from NASA FIRMS satellites. |
| | |
| | Args: |
| | lat: Latitude coordinate |
| | lon: Longitude coordinate |
| | radius_km: Search radius in kilometers (default 50km) |
| | days: Number of days to look back (default 2 days) |
| | |
| | Returns: |
| | Dict with raw fire detection data from NASA satellites |
| | """ |
| | if not NASA_FIRMS_MAP_KEY or NASA_FIRMS_MAP_KEY == "your-nasa-firms-api-key-here": |
| | return {"error": "NASA FIRMS API key not configured in .env file"} |
| |
|
| | try: |
| | lat_offset = radius_km / 111.0 |
| | lon_offset = radius_km / (111.0 * abs(math.cos(math.radians(lat)))) |
| | bbox = f"{lat - lat_offset},{lon - lon_offset},{lat + lat_offset},{lon + lon_offset}" |
| |
|
| | modis_url = f"https://firms.modaps.eosdis.nasa.gov/api/area/csv/{NASA_FIRMS_MAP_KEY}/MODIS_NRT/{bbox}/{days}" |
| | viirs_url = f"https://firms.modaps.eosdis.nasa.gov/api/area/csv/{NASA_FIRMS_MAP_KEY}/VIIRS_NOAA20_NRT/{bbox}/{days}" |
| |
|
| | all_fires = [] |
| |
|
| | try: |
| | modis_response = requests.get(modis_url, timeout=15) |
| | if modis_response.status_code == 200 and modis_response.text.strip(): |
| | all_fires.extend(_parse_nasa_csv(modis_response.text, "MODIS")) |
| | except: |
| | pass |
| |
|
| | try: |
| | viirs_response = requests.get(viirs_url, timeout=15) |
| | if viirs_response.status_code == 200 and viirs_response.text.strip(): |
| | all_fires.extend(_parse_nasa_csv(viirs_response.text, "VIIRS")) |
| | except: |
| | pass |
| |
|
| | return { |
| | "fires": all_fires, |
| | "query_location": { |
| | "lat": lat, |
| | "lon": lon, |
| | "radius_km": radius_km, |
| | "days": days, |
| | }, |
| | "data_source": "NASA_FIRMS", |
| | } |
| |
|
| | except Exception as e: |
| | return {"error": str(e)} |
| |
|
| |
|
| | def _parse_nasa_csv(csv_text: str, source: str) -> list: |
| | """Parse NASA FIRMS CSV data. |
| | |
| | Args: |
| | csv_text: CSV text data from NASA FIRMS API |
| | source: Source identifier (MODIS or VIIRS) |
| | |
| | Returns: |
| | List of fire detection dictionaries |
| | """ |
| | fires = [] |
| | lines = csv_text.strip().split("\n") |
| |
|
| | if len(lines) < 2: |
| | return fires |
| |
|
| | for line in lines[1:]: |
| | try: |
| | values = line.split(",") |
| | if len(values) >= 9: |
| | fires.append( |
| | { |
| | "latitude": float(values[0]), |
| | "longitude": float(values[1]), |
| | "brightness": float(values[2]) if values[2] else 0, |
| | "scan": float(values[3]) if values[3] else 0, |
| | "track": float(values[4]) if values[4] else 0, |
| | "acq_date": values[5], |
| | "acq_time": values[6], |
| | "satellite": values[7], |
| | "confidence": int(values[8]) if values[8].isdigit() else 50, |
| | "version": values[9] if len(values) > 9 else "", |
| | "bright_t31": ( |
| | float(values[10]) if len(values) > 10 and values[10] else 0 |
| | ), |
| | "frp": ( |
| | float(values[11]) if len(values) > 11 and values[11] else 0 |
| | ), |
| | "daynight": values[12] if len(values) > 12 else "", |
| | "source": source, |
| | } |
| | ) |
| | except (ValueError, IndexError): |
| | continue |
| |
|
| | return fires |
| |
|
| |
|
| | @tool |
| | def find_local_emergency_resources(lat: float, lon: float) -> dict: |
| | """Find local emergency resources and contacts. |
| | |
| | Args: |
| | lat: Latitude coordinate |
| | lon: Longitude coordinate |
| | |
| | Returns: |
| | Dict with local emergency resources or error message |
| | """ |
| | try: |
| | query = f""" |
| | [out:json][timeout:15]; |
| | ( |
| | node[amenity=hospital](around:10000,{lat},{lon}); |
| | node[amenity=fire_station](around:10000,{lat},{lon}); |
| | node[amenity=police](around:10000,{lat},{lon}); |
| | ); |
| | out center meta; |
| | """ |
| |
|
| | response = requests.post( |
| | "https://overpass-api.de/api/interpreter", data=query, timeout=20 |
| | ) |
| |
|
| | if response.status_code == 200: |
| | data = response.json() |
| | resources = [] |
| |
|
| | for element in data.get("elements", [])[:5]: |
| | tags = element.get("tags", {}) |
| | resources.append( |
| | { |
| | "name": tags.get("name", "Unnamed facility"), |
| | "type": tags.get("amenity", "unknown"), |
| | "latitude": element.get("lat", lat), |
| | "longitude": element.get("lon", lon), |
| | } |
| | ) |
| |
|
| | return {"local_resources": resources} |
| |
|
| | return {"local_resources": []} |
| |
|
| | except Exception as e: |
| | return {"error": str(e)} |
| |
|
| |
|
| | @tool |
| | def generate_analysis_report( |
| | data: dict, filename: str = "climate_risk_report.pdf" |
| | ) -> dict: |
| | """Generate a consolidated analysis report with visualizations. |
| | |
| | Args: |
| | data: Consolidated data from various tools, expected to include: |
| | - weather forecast |
| | - flood data |
| | - earthquake data |
| | - fire data |
| | filename: Desired filename for the exported PDF report |
| | |
| | Returns: |
| | Dict with success message and file path or error |
| | """ |
| | try: |
| | |
| | with tempfile.TemporaryDirectory() as temp_dir: |
| | |
| | pdf = FPDF() |
| | pdf.set_auto_page_break(auto=True, margin=15) |
| | pdf.add_page() |
| | pdf.set_font("Arial", size=12) |
| | pdf.set_text_color(50, 50, 50) |
| |
|
| | |
| | pdf.set_font("Arial", style="B", size=16) |
| | pdf.cell(0, 10, "Climate Risk Analysis Report", ln=True, align="C") |
| | pdf.ln(10) |
| |
|
| | |
| | def save_plot(fig, plot_name): |
| | path = f"{temp_dir}/{plot_name}.png" |
| | fig.savefig(path) |
| | plt.close(fig) |
| | return path |
| |
|
| | |
| | weather_data = data.get("weather_forecast", {}).get("daily", {}) |
| | if weather_data: |
| | dates = [ |
| | d for d in range(1, len(weather_data["temperature_2m_max"]) + 1) |
| | ] |
| | weather_df = { |
| | "Day": dates, |
| | "Max Temperature (°C)": weather_data["temperature_2m_max"], |
| | "Min Temperature (°C)": weather_data["temperature_2m_min"], |
| | "Precipitation (mm)": weather_data["precipitation_sum"], |
| | } |
| |
|
| | fig, ax = plt.subplots(figsize=(8, 5)) |
| | sns.lineplot( |
| | x="Day", |
| | y="Max Temperature (°C)", |
| | data=weather_df, |
| | ax=ax, |
| | label="Max Temp", |
| | color="red", |
| | ) |
| | sns.lineplot( |
| | x="Day", |
| | y="Min Temperature (°C)", |
| | data=weather_df, |
| | ax=ax, |
| | label="Min Temp", |
| | color="blue", |
| | ) |
| | sns.barplot( |
| | x="Day", |
| | y="Precipitation (mm)", |
| | data=weather_df, |
| | ax=ax, |
| | color="gray", |
| | alpha=0.5, |
| | ) |
| | ax.set_title("Weather Forecast") |
| | ax.set_xlabel("Day") |
| | ax.set_ylabel("Values") |
| | ax.legend() |
| |
|
| | weather_plot_path = save_plot(fig, "weather_plot") |
| | pdf.image(weather_plot_path, x=10, y=None, w=180) |
| | pdf.ln(10) |
| |
|
| | |
| | earthquake_data = data.get("earthquake_data", {}).get("earthquakes", []) |
| | if earthquake_data: |
| | magnitudes = [ |
| | eq["magnitude"] for eq in earthquake_data if eq.get("magnitude") |
| | ] |
| | depths = [eq["depth"] for eq in earthquake_data if eq.get("depth")] |
| | places = [eq["place"] for eq in earthquake_data] |
| |
|
| | fig, ax = plt.subplots(figsize=(8, 5)) |
| | sns.scatterplot( |
| | x=depths, y=magnitudes, hue=places, ax=ax, palette="tab10", s=100 |
| | ) |
| | ax.set_title("Earthquake Analysis") |
| | ax.set_xlabel("Depth (km)") |
| | ax.set_ylabel("Magnitude") |
| | ax.legend(bbox_to_anchor=(1.05, 1), loc="upper left") |
| |
|
| | earthquake_plot_path = save_plot(fig, "earthquake_plot") |
| | pdf.image(earthquake_plot_path, x=10, y=None, w=180) |
| | pdf.ln(10) |
| |
|
| | |
| | fire_data = data.get("fire_data", {}).get("fires", []) |
| | if fire_data: |
| | brightness = [fire["brightness"] for fire in fire_data] |
| | confidence = [fire["confidence"] for fire in fire_data] |
| |
|
| | fig, ax = plt.subplots(figsize=(8, 5)) |
| | sns.histplot( |
| | brightness, |
| | bins=20, |
| | ax=ax, |
| | kde=True, |
| | color="orange", |
| | label="Brightness", |
| | ) |
| | sns.histplot( |
| | confidence, |
| | bins=20, |
| | ax=ax, |
| | kde=True, |
| | color="green", |
| | alpha=0.5, |
| | label="Confidence", |
| | ) |
| | ax.set_title("Wildfire Brightness vs Confidence") |
| | ax.set_xlabel("Value") |
| | ax.legend() |
| |
|
| | fire_plot_path = save_plot(fig, "fire_plot") |
| | pdf.image(fire_plot_path, x=10, y=None, w=180) |
| | pdf.ln(10) |
| |
|
| | |
| | pdf_output_path = os.path.join(temp_dir, filename) |
| | pdf.output(pdf_output_path) |
| | return {"success": True, "file_path": pdf_output_path} |
| |
|
| | except Exception as e: |
| | return {"error": str(e)} |
| |
|
| |
|
| | @tool |
| | def get_full_daily_forecast(lat: float, lon: float) -> dict: |
| | """ |
| | Get all available daily weather forecast parameters from Open-Meteo API. |
| | Args: |
| | lat: Latitude. |
| | lon: Longitude. |
| | Returns: |
| | Dict with all daily forecast data or error. |
| | """ |
| | daily_params = [ |
| | "temperature_2m_max", "temperature_2m_mean", "temperature_2m_min", |
| | "apparent_temperature_max", "apparent_temperature_mean", "apparent_temperature_min", |
| | "precipitation_sum", "rain_sum", "showers_sum", "snowfall_sum", |
| | "precipitation_hours", |
| | "precipitation_probability_max", "precipitation_probability_mean", "precipitation_probability_min", |
| | "weather_code", "sunrise", "sunset", |
| | "sunshine_duration", "daylight_duration", |
| | "wind_speed_10m_max", "wind_gusts_10m_max", "wind_direction_10m_dominant", |
| | "shortwave_radiation_sum", "et0_fao_evapotranspiration", |
| | "uv_index_max", "uv_index_clear_sky_max" |
| | ] |
| | url = "https://api.open-meteo.com/v1/forecast" |
| | params = { |
| | "latitude": lat, |
| | "longitude": lon, |
| | "timezone": "auto", |
| | "daily": ",".join(daily_params) |
| | } |
| | try: |
| | response = requests.get(url, params=params, timeout=10) |
| | return response.json() |
| | except Exception as e: |
| | return {"error": str(e)} |
| |
|
| |
|
| | @tool |
| | def climate_change_data( |
| | lat: float, |
| | lon: float, |
| | start_date: str = "1950-01-01", |
| | end_date: str = "2050-12-31", |
| | models: list[str] = None |
| | ) -> dict: |
| | """ |
| | Get all available daily climate parameters from Open-Meteo Climate API. |
| | Args: |
| | lat: Latitude. |
| | lon: Longitude. |
| | start_date: Start date in yyyy-mm-dd (default 1950-01-01). |
| | end_date: End date in yyyy-mm-dd (default 2050-12-31). |
| | models: Optional list of climate models (default: all models). |
| | Returns: |
| | Dict with all daily climate data or error. |
| | """ |
| | daily_params = [ |
| | "temperature_2m_max", "temperature_2m_min", "temperature_2m_mean", |
| | "cloud_cover_mean", |
| | "relative_humidity_2m_max", "relative_humidity_2m_min", "relative_humidity_2m_mean", |
| | "soil_moisture_0_to_10cm_mean", |
| | "precipitation_sum", "rain_sum", "snowfall_sum", |
| | "wind_speed_10m_mean", "wind_speed_10m_max", |
| | "pressure_msl_mean", |
| | "shortwave_radiation_sum" |
| | ] |
| | if models is None: |
| | models = [ |
| | "CMCC_CM2_VHR4", "FGOALS_f3_H", "HiRAM_SIT_HR", |
| | "MRI_AGCM3_2_S", "EC_Earth3P_HR", "MPI_ESM1_2_XR", "NICAM16_8S" |
| | ] |
| | url = "https://climate-api.open-meteo.com/v1/climate" |
| | params = { |
| | "latitude": lat, |
| | "longitude": lon, |
| | "start_date": start_date, |
| | "end_date": end_date, |
| | "models": ",".join(models), |
| | "daily": ",".join(daily_params), |
| | "timezone": "auto" |
| | } |
| | try: |
| | response = requests.get(url, params=params, timeout=60) |
| | return response.json() |
| | except Exception as e: |
| | return {"error": str(e)} |
| |
|
| |
|
| | @tool |
| | def get_full_air_quality_forecast( |
| | lat: float, |
| | lon: float, |
| | forecast_days: int = 5, |
| | past_days: int = 0, |
| | domain: str = "auto" |
| | ) -> dict: |
| | """ |
| | Get all available hourly air quality forecast parameters from Open-Meteo Air Quality API. |
| | Args: |
| | lat: Latitude. |
| | lon: Longitude. |
| | forecast_days: Number of forecast days (default 5, max 7). |
| | past_days: Number of past days (default 0, max 92). |
| | domain: 'auto', 'cams_europe', or 'cams_global'. |
| | Returns: |
| | Dict with all hourly air quality data or error. |
| | """ |
| | hourly_params = [ |
| | "pm10", "pm2_5", "carbon_monoxide", "carbon_dioxide", |
| | "nitrogen_dioxide", "sulphur_dioxide", "ozone", "aerosol_optical_depth", |
| | "dust", "uv_index", "uv_index_clear_sky", "ammonia", "methane", |
| | "alder_pollen", "birch_pollen", "grass_pollen", "mugwort_pollen", |
| | "olive_pollen", "ragweed_pollen", "european_aqi", "us_aqi" |
| | ] |
| |
|
| | url = "https://air-quality-api.open-meteo.com/v1/air-quality" |
| | params = { |
| | "latitude": lat, |
| | "longitude": lon, |
| | "forecast_days": min(max(forecast_days, 0), 7), |
| | "past_days": min(max(past_days, 0), 92), |
| | "hourly": ",".join(hourly_params), |
| | "domains": domain, |
| | "timezone": "auto", |
| | } |
| | try: |
| | response = requests.get(url, params=params, timeout=30) |
| | return response.json() |
| | except Exception as e: |
| | return {"error": str(e)} |
| |
|
| |
|
| | @tool |
| | def get_full_marine_daily_forecast(lat: float, lon: float) -> dict: |
| | """ |
| | Get all available daily marine forecast parameters from Open-Meteo Marine API. |
| | Args: |
| | lat: Latitude. |
| | lon: Longitude. |
| | Returns: |
| | Dict with all daily marine forecast data or error. |
| | """ |
| | daily_params = [ |
| | "wave_height_max", "wind_wave_height_max", "swell_wave_height_max", |
| | "wave_direction_dominant", "wind_wave_direction_dominant", "swell_wave_direction_dominant", |
| | "wave_period_max", "wind_wave_period_max", "swell_wave_period_max", |
| | "wind_wave_peak_period_max", "swell_wave_peak_period_max" |
| | ] |
| | url = "https://marine-api.open-meteo.com/v1/marine" |
| | params = { |
| | "latitude": lat, |
| | "longitude": lon, |
| | "timezone": "auto", |
| | "daily": ",".join(daily_params) |
| | } |
| | try: |
| | response = requests.get(url, params=params, timeout=10) |
| | return response.json() |
| | except Exception as e: |
| | return {"error": str(e)} |
| |
|
| |
|
| | @tool |
| | def get_full_flood_daily_forecast(lat: float, lon: float) -> dict: |
| | """ |
| | Get all available daily flood parameters from Open-Meteo Flood API. |
| | Args: |
| | lat: Latitude. |
| | lon: Longitude. |
| | Returns: |
| | Dict with all daily flood forecast data or error. |
| | """ |
| | daily_params = [ |
| | "river_discharge", |
| | "river_discharge_mean", |
| | "river_discharge_median", |
| | "river_discharge_max", |
| | "river_discharge_min", |
| | "river_discharge_p25", |
| | "river_discharge_p75" |
| | ] |
| | url = "https://flood-api.open-meteo.com/v1/flood" |
| | params = { |
| | "latitude": lat, |
| | "longitude": lon, |
| | "daily": ",".join(daily_params) |
| | } |
| | try: |
| | response = requests.get(url, params=params, timeout=10) |
| | return response.json() |
| | except Exception as e: |
| | return {"error": str(e)} |
| |
|
| | @tool |
| | def get_full_satellite_radiation( |
| | lat: float, |
| | lon: float, |
| | start_date: str = None, |
| | end_date: str = None, |
| | hourly_native: bool = False, |
| | tilt: int = 0, |
| | azimuth: int = 0 |
| | ) -> dict: |
| | """ |
| | Get all available hourly satellite solar radiation parameters from Open-Meteo Satellite API. |
| | Args: |
| | lat: Latitude. |
| | lon: Longitude. |
| | start_date: (optional) Start date (yyyy-mm-dd). If None, today. |
| | end_date: (optional) End date (yyyy-mm-dd). If None, today. |
| | hourly_native: Use native satellite temporal resolution (10/15/30min) if True, else hourly. |
| | tilt: Tilt for GTI (default 0 = horizontal). |
| | azimuth: Azimuth for GTI (default 0 = south). |
| | Returns: |
| | Dict with all hourly satellite solar radiation data or error. |
| | """ |
| | hourly_params = [ |
| | "shortwave_radiation", "diffuse_radiation", "direct_radiation", |
| | "direct_normal_irradiance", "global_tilted_irradiance", |
| | "terrestrial_radiation", |
| | "shortwave_radiation_instant", "diffuse_radiation_instant", "direct_radiation_instant", |
| | "direct_normal_irradiance_instant", "global_tilted_irradiance_instant", |
| | "terrestrial_radiation_instant" |
| | ] |
| | url = "https://satellite-api.open-meteo.com/v1/archive" |
| |
|
| | today = datetime.utcnow().date() |
| | if start_date is None: |
| | start_date = str(today) |
| | if end_date is None: |
| | end_date = str(today) |
| |
|
| | params = { |
| | "latitude": lat, |
| | "longitude": lon, |
| | "start_date": start_date, |
| | "end_date": end_date, |
| | "hourly": ",".join(hourly_params), |
| | "models": "satellite_radiation_seamless", |
| | "timezone": "auto", |
| | "tilt": tilt, |
| | "azimuth": azimuth, |
| | } |
| | if hourly_native: |
| | params["hourly_native"] = "true" |
| |
|
| | try: |
| | response = requests.get(url, params=params, timeout=30) |
| | return response.json() |
| | except Exception as e: |
| | return {"error": str(e)} |
| |
|