File size: 8,276 Bytes
7b6b271
23a9367
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b6b271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23a9367
 
 
 
 
 
 
 
7b6b271
 
 
 
 
 
 
 
 
23a9367
 
 
 
 
 
 
 
 
 
 
 
 
 
7b6b271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
"""
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,
    }