Spaces:
Sleeping
Sleeping
| """ | |
| Stormglass Marine Data Tool - Premium Wave and Wind Conditions. | |
| This module provides access to high-quality marine data from the Stormglass API, | |
| with intelligent fallback to Open-Meteo for reliability. It fetches real-time | |
| and forecast data including waves, wind, swell, and water temperature. | |
| Data Sources (Priority Order): | |
| 1. Stormglass API: Premium accuracy, requires API key | |
| 2. Open-Meteo Marine: Free fallback, always available | |
| Supported Parameters: | |
| - Wave height, direction, and period | |
| - Wind speed, direction, and gusts | |
| - Swell direction and characteristics | |
| - Water temperature (Stormglass only) | |
| - Forecast data up to 10 days | |
| The tool automatically handles API failures, rate limits, and | |
| missing API keys by falling back to the free Open-Meteo service. | |
| Example: | |
| >>> tool = create_stormglass_tool()["function"] | |
| >>> result = tool(WaveDataInput(lat=36.72, lon=-4.42)) | |
| >>> print(f"Wave height: {result.wave_height}m") | |
| Author: Surf Spot Finder Team | |
| License: MIT | |
| """ | |
| import os | |
| from datetime import datetime, timedelta | |
| from typing import Optional | |
| from .marine_data_tool import OpenMeteoMarineAPI | |
| import requests | |
| from pydantic import BaseModel, Field | |
| # ------------------------------------------------------- | |
| # Input / Output Schemas | |
| # ------------------------------------------------------- | |
| class WaveDataInput(BaseModel): | |
| """Input schema for wave data requests. | |
| Attributes: | |
| lat: Latitude in decimal degrees (-90 to 90). | |
| lon: Longitude in decimal degrees (-180 to 180). | |
| target_datetime: Optional future datetime for forecast data. | |
| If None, returns current conditions. | |
| """ | |
| lat: float = Field(description="Latitude of the surf spot") | |
| lon: float = Field(description="Longitude of the surf spot") | |
| target_datetime: Optional[datetime] = Field( | |
| None, | |
| description="Datetime for forecast. If none, returns current conditions." | |
| ) | |
| class WaveDataOutput(BaseModel): | |
| """Output schema for marine condition data. | |
| Attributes: | |
| wave_height: Significant wave height in meters. | |
| wave_direction: Wave direction in degrees (0-360). | |
| wave_period: Wave period in seconds. | |
| wind_speed: Wind speed in meters per second. | |
| wind_direction: Wind direction in degrees (0-360). | |
| swell_direction: Primary swell direction in degrees. | |
| wind_gust: Maximum wind gust speed in m/s. | |
| water_temperature: Water temperature in Celsius. | |
| timestamp: Data timestamp in ISO format. | |
| forecast_target: Target forecast time if applicable. | |
| """ | |
| wave_height: float | |
| wave_direction: float | |
| wave_period: float | |
| wind_speed: float | |
| wind_direction: float | |
| swell_direction: float | |
| wind_gust: float | |
| water_temperature: float | |
| timestamp: Optional[str] | |
| forecast_target: Optional[str] | |
| data_source: str | |
| # ------------------------------------------------------- | |
| # Stormglass Tool | |
| # ------------------------------------------------------- | |
| class StormglassTool: | |
| name = "fetch_wave_data" | |
| description = "Fetch wave, wind, swell, and marine conditions using Stormglass API" | |
| BASE_URL = "https://api.stormglass.io/v2/weather/point" | |
| PARAMS = "waveHeight,waterTemperature,windSpeed,windDirection,windWaveDirection,swellDirection,gust,wavePeriod" | |
| def __init__(self): | |
| self.api_key = os.getenv("STORMGLASS_API_KEY") | |
| def run(self, input_data: WaveDataInput) -> WaveDataOutput: | |
| if not self.api_key: | |
| # No API key? fallback directly | |
| return self._fallback(input_data) | |
| payload = { | |
| "lat": input_data.lat, | |
| "lng": input_data.lon, | |
| "params": self.PARAMS, | |
| } | |
| # If forecast requested: add start/end timestamps | |
| if input_data.target_datetime: | |
| start_time = input_data.target_datetime.isoformat() | |
| end_time = (input_data.target_datetime + timedelta(hours=1)).isoformat() | |
| payload["start"] = start_time | |
| payload["end"] = end_time | |
| try: | |
| response = requests.get( | |
| self.BASE_URL, | |
| params=payload, | |
| headers={ | |
| "Authorization": self.api_key, | |
| "Accept": "application/json" | |
| } | |
| ) | |
| if response.status_code != 200: | |
| return self._fallback(input_data) | |
| data = response.json() | |
| hours = data.get("hours", []) | |
| if not hours: | |
| return self._fallback(input_data) | |
| # Choose closest hour to target, or first | |
| if input_data.target_datetime and len(hours) > 1: | |
| target_hour = input_data.target_datetime.hour | |
| hour = min( | |
| hours, | |
| key=lambda h: abs( | |
| datetime.fromisoformat( | |
| h["time"].replace("Z", "+00:00") | |
| ).hour - target_hour | |
| ) | |
| ) | |
| else: | |
| hour = hours[0] | |
| return WaveDataOutput( | |
| wave_height=round(hour["waveHeight"]["sg"], 2), | |
| wave_direction=round(hour["windWaveDirection"]["sg"], 2), | |
| wave_period=round(hour.get("wavePeriod", {}).get("sg", 0), 2), | |
| wind_speed=round(hour["windSpeed"]["sg"], 2), | |
| wind_direction=round(hour["windDirection"]["sg"], 2), | |
| swell_direction=round(hour.get("swellDirection", {}).get("sg", 0), 2), | |
| wind_gust=round(hour.get("gust", {}).get("sg", 0), 2), | |
| water_temperature=round(hour["waterTemperature"]["sg"], 2), | |
| timestamp=hour.get("time"), | |
| forecast_target=input_data.target_datetime.isoformat() if input_data.target_datetime else None, | |
| data_source="stormglass" | |
| ) | |
| except Exception: | |
| return self._fallback(input_data) | |
| # ------------------------------------------------------- | |
| # Fallback (Open-Meteo) | |
| # ------------------------------------------------------- | |
| def _fallback(self, input_data: WaveDataInput) -> WaveDataOutput: | |
| """ | |
| Calls the marine_data_tool fallback β converts any lists to single values for Pydantic. | |
| """ | |
| from .marine_data_tool import OpenMeteoMarineAPI | |
| api = OpenMeteoMarineAPI() | |
| if input_data.target_datetime: | |
| forecast_data = api.get_forecast_for_hour( | |
| lat=input_data.lat, | |
| lon=input_data.lon, | |
| target_datetime=input_data.target_datetime | |
| ) | |
| else: | |
| forecast_data = api.get_current_conditions( | |
| lat=input_data.lat, | |
| lon=input_data.lon | |
| ) | |
| # Helper to pick first element if list, fallback to 0 if None | |
| def to_float(val): | |
| if isinstance(val, list): | |
| return float(val[0]) if val else 0.0 | |
| if val is None: | |
| return 0.0 | |
| return float(val) | |
| return WaveDataOutput( | |
| wave_height=to_float(forecast_data.get("wave_height")), | |
| wave_direction=to_float(forecast_data.get("wave_direction")), | |
| wave_period=to_float(forecast_data.get("wave_period")), | |
| wind_speed=to_float(forecast_data.get("wind_speed")), | |
| wind_direction=to_float(forecast_data.get("wind_direction")), | |
| swell_direction=to_float(forecast_data.get("swell_direction")), | |
| wind_gust=to_float(forecast_data.get("wind_gust")), | |
| water_temperature=to_float(forecast_data.get("water_temperature")), | |
| timestamp=forecast_data.get("timestamp"), | |
| forecast_target=forecast_data.get("forecast_target"), | |
| data_source="open-meteo" | |
| ) | |
| # ------------------------------------------------------- | |
| # Registration | |
| # ------------------------------------------------------- | |
| def create_stormglass_tool(): | |
| tool = StormglassTool() | |
| return { | |
| "name": tool.name, | |
| "description": tool.description, | |
| "input_schema": WaveDataInput.schema(), | |
| "function": tool.run, | |
| } | |