44
55import logging
66from concurrent .futures import ThreadPoolExecutor
7- from datetime import datetime , timedelta
7+ from datetime import datetime , timedelta , timezone
88from functools import lru_cache
99from http import HTTPStatus
1010from threading import Lock
1414import requests
1515from cachetools import TTLCache , cached
1616from geopy .geocoders import Nominatim
17+ from geopy .distance import great_circle
1718
1819from src import helper
1920from src .open_meteo import openmeteo_client
3233_rain_cache = TTLCache (maxsize = _MAXSIZE , ttl = _TTL )
3334forecast_cache = TTLCache (maxsize = _MAXSIZE , ttl = _TTL )
3435_hourlyforecast_cache = TTLCache (maxsize = _MAXSIZE , ttl = _TTL )
36+ _tide_cache = TTLCache (maxsize = _MAXSIZE , ttl = _TTL )
3537_ocean_lock = Lock ()
3638
3739
40+
3841@lru_cache (maxsize = 128 )
3942def get_coordinates (args : tuple ) -> list | str :
4043 """
@@ -183,7 +186,7 @@ def ocean_information(
183186 params = {
184187 "latitude" : lat ,
185188 "longitude" : long ,
186- "current" : ["wave_height" , "wave_direction" , "wave_period" ],
189+ "current" : ["wave_height" , "wave_direction" , "wave_period" , "sea_surface_temperature" ],
187190 "length_unit" : unit ,
188191 "timezone" : "auto" ,
189192 "forecast_days" : 3 ,
@@ -202,8 +205,9 @@ def ocean_information(
202205 current_wave_height = round (current .Variables (0 ).Value (), decimal )
203206 current_wave_direction = round (current .Variables (1 ).Value (), decimal )
204207 current_wave_period = round (current .Variables (2 ).Value (), decimal )
208+ current_sea_surface_temperature = current .Variables (3 ).Value ()
205209
206- return [current_wave_height , current_wave_direction , current_wave_period ]
210+ return [current_wave_height , current_wave_direction , current_wave_period , current_sea_surface_temperature ]
207211
208212
209213@cached (ocean_history_cache , lock = _ocean_lock )
@@ -482,6 +486,69 @@ def get_hourly_forecast(
482486
483487 return curr_hour_data
484488
489+ @cached (_tide_cache , lock = _ocean_lock )
490+ def get_tide_data (lat : float , long : float ):
491+ """
492+ Fetches tide data for the given cords
493+ """
494+ station_id , station_distance = nearest_station (lat , long )
495+ begin = (datetime .now (timezone .utc ) - timedelta (days = 1 )).strftime ("%Y%m%d" )
496+
497+
498+ url = "https://api.tidesandcurrents.noaa.gov/api/prod/datagetter"
499+ params = {
500+ "station" : station_id ,
501+ "product" : "predictions" ,
502+ "datum" : "MLLW" ,
503+ "interval" : "hilo" , # high/low only; omit for 6-min intervals
504+ "units" : "english" ,
505+ "time_zone" : "gmt" ,
506+ "format" : "json" ,
507+ "begin_date" : begin ,
508+ "range" : 72 , # hours
509+ }
510+ r = requests .get (url , params = params , timeout = 10 )
511+ r .raise_for_status ()
512+ return r .json ()
513+
514+
515+ @lru_cache (maxsize = 1 )
516+ def _get_stations ():
517+ url = "https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/stations.json"
518+ r = requests .get (url , params = {"type" : "tidepredictions" }, timeout = 10 )
519+ r .raise_for_status ()
520+ return r .json ()["stations" ]
521+
522+ def nearest_station (lat : float , long : float ) -> tuple [str , float ]:
523+ """
524+ returns the id of the nearest NOAA station in respect to the given cords
525+ """
526+ min_station_distance = float ("inf" )
527+ nearest_id = None
528+ surf_spot_cords = (float (lat ), float (long ))
529+
530+ stations = _get_stations ()
531+
532+ for station in stations :
533+ station_cords = (float (station ["lat" ]), float (station ["lng" ]))
534+ station_id = station ["id" ]
535+ distance = great_circle (surf_spot_cords , station_cords ).mi
536+ if distance < min_station_distance :
537+ min_station_distance = distance
538+ nearest_id = station_id
539+
540+ if nearest_id is None :
541+ raise RuntimeError ("No stations returned" )
542+
543+ return nearest_id , min_station_distance
544+
545+
546+ def _safe_current_tide (lat : float , long : float ):
547+ try :
548+ return helper .current_tide (lat , long )
549+ except Exception :
550+ return None
551+
485552
486553def gather_data (lat : float | str , long : float | str , arguments : dict ) -> dict :
487554 """
@@ -492,7 +559,7 @@ def gather_data(lat: float | str, long: float | str, arguments: dict) -> dict:
492559 lat , long = float (lat ), float (long )
493560 dec , unit = arguments ["decimal" ], arguments ["unit" ]
494561
495- with ThreadPoolExecutor (max_workers = 8 ) as executor :
562+ with ThreadPoolExecutor (max_workers = 9 ) as executor :
496563 futures = {
497564 "ocean" : executor .submit (ocean_information , lat , long , dec , unit ),
498565 "uv" : executor .submit (get_uv , lat , long , dec , unit ),
@@ -504,6 +571,7 @@ def gather_data(lat: float | str, long: float | str, arguments: dict) -> dict:
504571 ocean_information_history , lat , long , dec , unit
505572 ),
506573 "uv_hist" : executor .submit (get_uv_history , lat , long , dec , unit ),
574+ "tide" : executor .submit (_safe_current_tide , lat , long ),
507575 }
508576 results = {k : f .result () for k , f in futures .items ()}
509577
@@ -514,6 +582,12 @@ def gather_data(lat: float | str, long: float | str, arguments: dict) -> dict:
514582 rain_sum , precipitation_probability_max = results ["rain" ]
515583 json_forecast = helper .forecast_to_json (results ["forecast" ], dec )
516584
585+ sea_temp_c = results ["ocean" ][3 ]
586+ if unit == "imperial" :
587+ sea_temp = round (sea_temp_c * 9 / 5 + 32 , dec )
588+ else :
589+ sea_temp = round (sea_temp_c , dec )
590+
517591 return {
518592 "Lat" : lat ,
519593 "Long" : long ,
@@ -535,6 +609,8 @@ def gather_data(lat: float | str, long: float | str, arguments: dict) -> dict:
535609 "Precipitation Probability Max" : precipitation_probability_max ,
536610 "Cloud Cover" : results ["hourly" ]["cloud_cover" ],
537611 "Visibility" : results ["hourly" ]["visibility" ],
612+ "Tide" : results ["tide" ],
613+ "Sea Surface Temperature" : sea_temp ,
538614 }
539615
540616
@@ -556,3 +632,7 @@ def separate_args_and_get_location(args: list) -> dict:
556632
557633# Backward-compatible alias for the misspelled name
558634seperate_args_and_get_location = separate_args_and_get_location
635+
636+
637+ if __name__ == "__main__" :
638+ print (get_tide_data (40.741895 , - 73.989308 ))
0 commit comments