33import requests
44from dateutil .parser import parse
55from django .core .management .base import BaseCommand
6+ from django .utils import timezone
67from wagtail .models import Site
78
8- from forecastmanager .forecast_settings import ForecastSetting , WeatherCondition , ForecastPeriod , ForecastDataParameters
9- from forecastmanager .models import City , CityForecast , DataValue , Forecast
109from forecastmanager .constants import WEATHER_CONDITIONS_AS_DICT
10+ from forecastmanager .forecast_settings import (
11+ ForecastSetting ,
12+ WeatherCondition ,
13+ ForecastPeriod ,
14+ ForecastDataParameters
15+ )
16+ from forecastmanager .models import (
17+ City ,
18+ CityForecast ,
19+ DataValue ,
20+ Forecast
21+ )
1122
1223logger = logging .getLogger (__name__ )
1324
1425# Define the base URL for the Met Norway API
15- BASE_URL = "https://www.yr.no/api/v0/locations"
26+ BASE_URL = "https://api.met.no/weatherapi/locationforecast/2.0/complete"
27+
28+ DEFAULT_INSTANT_DATA_PARAMETERS = [
29+ {"parameter" : "air_pressure_at_sea_level" , "name" : "Air Pressure (Sea level)" , "parameter_unit" : "hPa" },
30+ {"parameter" : "air_temperature" , "name" : "Minimum Air Temperature" , "parameter_unit" : "°C" },
31+ {"parameter" : "wind_speed" , "name" : "Wind Speed" , "parameter_unit" : "m/s" },
32+ {"parameter" : "wind_from_direction" , "name" : "Wind Direction " , "parameter_unit" : "degrees" }
33+ ]
34+
35+ DEFAULT_NEXT_HOURS_DATA_PARAMETERS = [
36+ {"parameter" : "air_temperature_min" , "name" : "Minimum Air Temperature" , "parameter_unit" : "°C" },
37+ {"parameter" : "air_temperature_max" , "name" : "Maximum Air Temperature" , "parameter_unit" : "°C" },
38+ {"parameter" : "precipitation_amount" , "name" : "Precipitation Amount" , "parameter_unit" : "mm" },
39+ ]
40+
41+ DEFAULT_PARAMETERS = DEFAULT_INSTANT_DATA_PARAMETERS + DEFAULT_NEXT_HOURS_DATA_PARAMETERS
1642
1743
1844class Command (BaseCommand ):
1945 help = ('Get the weather forecast for the next 7 days from Yr.no '
2046 'for all cities in the database and save it to the database.' )
2147
2248 def handle (self , * args , ** options ):
23- print ("Getting 7 Day Forecast from Yr.no..." )
49+ logger . info ("Getting 7 Day Forecast from Yr.no..." )
2450
2551 cities = City .objects .all ()
2652 if not cities :
@@ -35,7 +61,13 @@ def handle(self, *args, **options):
3561
3662 forecast_setting = ForecastSetting .for_site (site )
3763
38- user_agent = f"{ site .site_name } (WMO NMHSs Website Template) { site .root_url } "
64+ site_name = site .site_name
65+ root_url = site .root_url
66+
67+ user_agent = f"ClimWeb { root_url } "
68+ if site_name :
69+ user_agent = f"{ site_name } /{ user_agent } "
70+
3971 user_agent = user_agent .strip ()
4072
4173 if not forecast_setting .enable_auto_forecast :
@@ -46,83 +78,60 @@ def handle(self, *args, **options):
4678 conditions_by_symbol = {condition .symbol : condition for condition in conditions }
4779
4880 parameters = forecast_setting .data_parameters .all ()
81+
4982 if not parameters .exists ():
5083 # create default forecast parameters
51- default_parameters = [
52- {"parameter" : "air_temperature_max" , "name" : "Maximum Air Temperature" , "parameter_unit" : "°C" },
53- {"parameter" : "air_temperature_min" , "name" : "Minimum Air Temperature" , "parameter_unit" : "°C" },
54- {"parameter" : "wind_speed" , "name" : "Wind Speed" , "parameter_unit" : "m/s" },
55- {"parameter" : "precipitation_amount" , "name" : "Precipitation Amount" , "parameter_unit" : "mm" }
56- ]
57-
58- for default_parameter in default_parameters :
84+ for default_parameter in DEFAULT_PARAMETERS :
5985 ForecastDataParameters .objects .create (parent = forecast_setting , ** default_parameter )
6086
6187 parameters = forecast_setting .data_parameters .all ()
6288
6389 parameters_dict = {parameter .parameter : parameter for parameter in parameters }
6490
65- forecast_periods = forecast_setting .periods .all ()
66- if not forecast_periods .exists ():
67- # create default forecast period
68- ForecastPeriod .objects .create (parent = forecast_setting ,
69- label = "Whole Day" ,
70- forecast_effective_time = "00:00:00" ,
71- default = True )
72-
73- forecast_period = forecast_periods .filter (default = True ).first ()
74- if not forecast_period :
75- # pick first one if no default
76- forecast_period = forecast_periods .first ()
77-
7891 cities_data = {}
7992
8093 for city in cities :
81- print (f"Getting forecast for { city .name } ..." )
94+ logger . info (f"Getting forecast for { city .name } ..." )
8295
8396 lon = city .x
8497 lat = city .y
8598
86- url = f"{ BASE_URL } / { lat } , { lon } /forecast "
99+ url = f"{ BASE_URL } ?lat= { lat } &lon= { lon } "
87100
88101 # Send a GET request to the API
89102 response = requests .get (url , headers = {"User-Agent" : user_agent })
90103
91104 if response .status_code >= 400 :
92- logger .error (
93- f"Failed to get forecast for { city .name } . Status code: { response .status_code } " )
105+ logger .error (f"Failed to get forecast for { city .name } . Status code: { response .status_code } " )
94106 continue
95107
96108 # Get the weather data from the response
97109 data = response .json ()
98110
99- day_intervals = data ['dayIntervals' ]
111+ # Get the timeseries data from the response
112+ timeseries = data .get ('properties' , {}).get ('timeseries' )
100113
101- for day in day_intervals [:8 ]:
102- date = parse (day .get ("start" ))
103- condition = day .get ("twentyFourHourSymbol" )
104- temperature_data = day .get ("temperature" )
105- air_temperature_max = temperature_data .get ("max" )
106- air_temperature_min = temperature_data .get ("min" )
114+ # Get the first and last datetime for the forecast
115+ first_datetime = timezone .localtime ().replace (hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
116+ max_days = 6
117+ last_datetime = (first_datetime + timezone .timedelta (days = max_days )).replace (hour = 23 , minute = 59 , second = 59 )
107118
108- wind_data = day .get ("wind" )
109- wind_speed = wind_data .get ("max" )
119+ # Create a forecast for the city
120+ for time_data in timeseries :
121+ time = time_data .get ("time" )
122+ utc_date = parse (time )
123+ timezone_date = timezone .localtime (utc_date )
110124
111- precipitation_data = day .get ("precipitation" )
112- precipitation = precipitation_data .get ("value" )
125+ # Check if the forecast is within the next 7 days
126+ if timezone_date < first_datetime or timezone_date > last_datetime :
127+ continue
113128
114- data_values = {
115- "date" : date ,
116- "condition" : condition ,
117- "parameters" : {
118- "air_temperature_max" : air_temperature_max ,
119- "air_temperature_min" : air_temperature_min ,
120- "wind_speed" : wind_speed ,
121- "precipitation_amount" : precipitation
122- }
123- }
129+ data_values = time_data .get ("data" , {})
124130
125- condition = data_values .get ('condition' )
131+ # Get the weather condition for the forecast
132+ condition = data_values .get ("next_1_hours" , {}).get ("summary" , {}).get ("symbol_code" )
133+ if condition is None :
134+ condition = data_values .get ("next_6_hours" , {}).get ("summary" , {}).get ("symbol_code" )
126135
127136 if conditions_by_symbol .get (condition ) is None :
128137 condition_info = WEATHER_CONDITIONS_AS_DICT .get (condition )
@@ -142,25 +151,58 @@ def handle(self, *args, **options):
142151 continue
143152
144153 city_forecast = CityForecast (city = city , condition = condition_obj )
145- for key , value in data_values .get ("parameters" , {}).items ():
154+
155+ instant_data = data_values .get ("instant" , {}).get ("details" , {})
156+ # Add the instant data values to the forecast
157+ for key , value in instant_data .items ():
146158 if parameters_dict .get (key ) is None :
147159 continue
148160
149161 parameter = parameters_dict [key ]
150162 data_value = DataValue (parameter = parameter , value = value )
151163 city_forecast .data_values .add (data_value )
152164
153- if date in cities_data :
154- cities_data [date ].append (city_forecast )
165+ # Add the next hours data values to the forecast
166+ for param in DEFAULT_NEXT_HOURS_DATA_PARAMETERS :
167+ param_key = param .get ("parameter" )
168+ if parameters_dict .get (param_key ) is None :
169+ continue
170+
171+ next_1_hours_data = data_values .get ("next_1_hours" , {}).get ("details" , {})
172+ next_6_hours_data = data_values .get ("next_6_hours" , {}).get ("details" , {})
173+
174+ next_data_value = None
175+ if param_key in next_1_hours_data :
176+ next_data_value = DataValue (parameter = parameters_dict [param_key ],
177+ value = next_1_hours_data [param_key ])
178+ elif param_key in next_6_hours_data :
179+ next_data_value = DataValue (parameter = parameters_dict [param_key ],
180+ value = next_6_hours_data [param_key ])
181+
182+ if next_data_value is not None :
183+ city_forecast .data_values .add (next_data_value )
184+
185+ # Add the forecast to the cities data
186+ if timezone_date in cities_data :
187+ cities_data [timezone_date ].append (city_forecast )
155188 else :
156- cities_data [date ] = [city_forecast ]
189+ cities_data [timezone_date ] = [city_forecast ]
190+
191+ # Create the forecast for the cities
192+ for forecast_date , city_forecasts in cities_data .items ():
193+ effective_time = f"{ forecast_date .hour } :00"
194+
195+ forecast_period = ForecastPeriod .objects .filter (forecast_effective_time = effective_time ).first ()
196+ if forecast_period is None :
197+ forecast_period = ForecastPeriod .objects .create (parent = forecast_setting ,
198+ forecast_effective_time = effective_time ,
199+ label = effective_time )
157200
158- for time , city_forecasts in cities_data .items ():
159- forecast = Forecast .objects .filter (forecast_date = time , effective_period = forecast_period )
201+ forecast = Forecast .objects .filter (forecast_date = forecast_date , effective_period = forecast_period )
160202 if forecast .exists ():
161203 forecast .delete ()
162204
163- forecast = Forecast (forecast_date = time , effective_period = forecast_period , source = "yr" )
205+ forecast = Forecast (forecast_date = forecast_date , effective_period = forecast_period , source = "yr" )
164206
165207 for city_forecast in city_forecasts :
166208 forecast .city_forecasts .add (city_forecast )
0 commit comments