Skip to content

Commit 359de46

Browse files
authored
Merge pull request #27 from wmo-raf/dev
Updates and Bug Fixes
2 parents 4842b0a + a8a18de commit 359de46

File tree

10 files changed

+246
-120
lines changed

10 files changed

+246
-120
lines changed

forecastmanager/forecast_settings.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def periods_as_choices(self):
6262
@property
6363
def effective_periods(self):
6464
return [
65-
{"label": period.label, "time": period.forecast_effective_time, "default": period.default}
65+
{"label": period.label, "time": period.forecast_effective_time}
6666
for period in self.periods.all()]
6767

6868
@property
@@ -73,27 +73,20 @@ def weather_conditions_list(self):
7373

7474
class ForecastPeriod(Orderable):
7575
parent = ParentalKey(ForecastSetting, on_delete=models.CASCADE, related_name="periods")
76-
default = models.BooleanField(default=False, verbose_name=_("Is default"))
77-
forecast_effective_time = models.TimeField(verbose_name=_("Forecast Effective Time"))
76+
forecast_effective_time = models.TimeField(verbose_name=_("Forecast Effective Time"), unique=True)
7877
label = models.CharField(max_length=100, verbose_name=_("Label"))
7978

8079
class Meta:
81-
unique_together = ("default", "forecast_effective_time")
80+
ordering = ["forecast_effective_time"]
8281

8382
panels = [
8483
FieldPanel('forecast_effective_time'),
8584
FieldPanel('label'),
86-
FieldPanel('default'),
8785
]
8886

8987
def __str__(self):
9088
return self.label
9189

92-
def save(self, *args, **kwargs):
93-
if self.default:
94-
ForecastPeriod.objects.filter(default=True).update(default=False)
95-
super().save(*args, **kwargs)
96-
9790

9891
class ForecastDataParameters(Orderable):
9992
PARAMETER_TYPE_CHOICES = (
@@ -136,7 +129,8 @@ def parameter_info(self):
136129
return WEATHER_PARAMETERS_AS_DICT.get(self.parameter)
137130

138131
def parse_value(self, value):
139-
# TODO: Implement parsing for different parameter types
132+
if self.parameter_type == "numeric":
133+
return float(value)
140134
return value
141135

142136

forecastmanager/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def clean(self):
9797

9898
# check parameters
9999
for param, value in params_data.items():
100-
if value:
100+
if value is not None and value != "":
101101
param = ForecastDataParameters.objects.filter(name=param).first()
102102
if not param:
103103
self.add_error(None, f"Unknown parameter found in table data: {param}")
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import logging
2+
3+
from django.core.management.base import BaseCommand
4+
from django.utils import timezone
5+
6+
from forecastmanager.models import Forecast
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
class Command(BaseCommand):
12+
help = 'Clear old forecasts from the database.'
13+
14+
def handle(self, *args, **options):
15+
logger.info("Clearing old forecasts from the database...")
16+
17+
current_time = timezone.localtime()
18+
19+
# Get all forecasts that are older than the current time
20+
old_forecasts = Forecast.objects.filter(forecast_date__lt=current_time)
21+
22+
if not old_forecasts:
23+
logger.info("No old forecasts found.")
24+
return
25+
26+
# Delete the old forecasts
27+
logger.info(f"Deleting {old_forecasts.count()} old forecasts...")
28+
old_forecasts.delete()
29+
30+
logger.info("Old forecasts deleted successfully.")

forecastmanager/management/commands/generate_forecast.py

Lines changed: 101 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,50 @@
33
import requests
44
from dateutil.parser import parse
55
from django.core.management.base import BaseCommand
6+
from django.utils import timezone
67
from wagtail.models import Site
78

8-
from forecastmanager.forecast_settings import ForecastSetting, WeatherCondition, ForecastPeriod, ForecastDataParameters
9-
from forecastmanager.models import City, CityForecast, DataValue, Forecast
109
from 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

1223
logger = 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

1844
class 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)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 4.2.7 on 2024-06-06 06:37
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('forecastmanager', '0024_alter_forecast_options_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AlterModelOptions(
14+
name='forecast',
15+
options={'ordering': ['forecast_date', 'effective_period'], 'verbose_name': 'Forecast', 'verbose_name_plural': 'Forecasts'},
16+
),
17+
migrations.AlterModelOptions(
18+
name='forecastperiod',
19+
options={'ordering': ['forecast_effective_time']},
20+
),
21+
]
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Generated by Django 4.2.3 on 2024-06-13 09:02
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('forecastmanager', '0025_alter_forecast_options_alter_forecastperiod_options'),
10+
]
11+
12+
operations = [
13+
migrations.AlterUniqueTogether(
14+
name='forecastperiod',
15+
unique_together=set(),
16+
),
17+
migrations.AlterField(
18+
model_name='forecastperiod',
19+
name='forecast_effective_time',
20+
field=models.TimeField(unique=True, verbose_name='Forecast Effective Time'),
21+
),
22+
migrations.RemoveField(
23+
model_name='forecastperiod',
24+
name='default',
25+
),
26+
]

0 commit comments

Comments
 (0)