meteostat 2.0.0__py3-none-any.whl → 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meteostat/__init__.py +10 -4
- meteostat/{core → api}/config.py +8 -5
- meteostat/api/interpolate.py +217 -79
- meteostat/api/stations.py +1 -1
- meteostat/core/cache.py +1 -1
- meteostat/core/data.py +4 -0
- meteostat/core/network.py +1 -1
- meteostat/enumerations.py +4 -0
- meteostat/interpolation/lapserate.py +1 -1
- meteostat/providers/dwd/climat.py +2 -2
- meteostat/providers/dwd/daily.py +1 -1
- meteostat/providers/dwd/hourly.py +1 -1
- meteostat/providers/dwd/monthly.py +6 -3
- meteostat/providers/dwd/shared.py +1 -1
- meteostat/providers/gsa/__init__.py +3 -0
- meteostat/providers/gsa/daily.py +194 -0
- meteostat/providers/gsa/hourly.py +175 -0
- meteostat/providers/gsa/monthly.py +192 -0
- meteostat/providers/gsa/synop.py +184 -0
- meteostat/providers/index.py +111 -0
- meteostat/providers/meteostat/daily.py +1 -1
- meteostat/providers/meteostat/hourly.py +1 -1
- meteostat/providers/meteostat/monthly.py +1 -1
- meteostat/providers/meteostat/shared.py +1 -1
- meteostat/providers/metno/forecast.py +16 -12
- meteostat/providers/noaa/ghcnd.py +1 -1
- meteostat/providers/noaa/isd_lite.py +14 -1
- meteostat/providers/noaa/metar.py +1 -1
- meteostat/utils/conversions.py +30 -31
- meteostat/utils/guards.py +51 -0
- meteostat/utils/parsers.py +0 -7
- {meteostat-2.0.0.dist-info → meteostat-2.1.0.dist-info}/METADATA +15 -19
- meteostat-2.1.0.dist-info/RECORD +69 -0
- {meteostat-2.0.0.dist-info → meteostat-2.1.0.dist-info}/WHEEL +1 -1
- meteostat-2.0.0.dist-info/RECORD +0 -63
- {meteostat-2.0.0.dist-info → meteostat-2.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GeoSphere Austria Data Hub daily data import routine
|
|
3
|
+
|
|
4
|
+
Get daily climate data for weather stations in Austria.
|
|
5
|
+
|
|
6
|
+
License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Dict, Optional
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
from meteostat.enumerations import TTL, Parameter
|
|
15
|
+
from meteostat.core.logger import logger
|
|
16
|
+
from meteostat.typing import ProviderRequest
|
|
17
|
+
from meteostat.core.cache import cache_service
|
|
18
|
+
from meteostat.api.config import config
|
|
19
|
+
from meteostat.core.network import network_service
|
|
20
|
+
from meteostat.utils.conversions import (
|
|
21
|
+
hours_to_minutes,
|
|
22
|
+
ms_to_kmh,
|
|
23
|
+
percentage_to_okta,
|
|
24
|
+
pres_to_msl,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
RESOURCE_ID = "klima-v2-1d"
|
|
29
|
+
|
|
30
|
+
# Mapping from GeoSphere Austria parameter names to Meteostat parameters
|
|
31
|
+
# See: https://dataset.api.hub.geosphere.at/v1/station/historical/klima-v2-1d/metadata
|
|
32
|
+
PARAMETER_MAPPING: Dict[str, Parameter] = {
|
|
33
|
+
"tl_mittel": Parameter.TEMP, # Mean air temperature (°C) - Lufttemperatur 2m Mittelwert
|
|
34
|
+
"tlmax": Parameter.TMAX, # Maximum air temperature (°C) - Lufttemperatur 2m Maximalwert
|
|
35
|
+
"tlmin": Parameter.TMIN, # Minimum air temperature (°C) - Lufttemperatur 2m Minimalwert
|
|
36
|
+
"rr": Parameter.PRCP, # Precipitation 24h sum (mm) - Niederschlag 24h Summe
|
|
37
|
+
"sh": Parameter.SNWD, # Snow depth (cm) - Schneehöhe
|
|
38
|
+
"vv_mittel": Parameter.WSPD, # Mean wind speed (km/h) - Windgeschwindigkeit Mittelwert
|
|
39
|
+
"ffx": Parameter.WPGT, # Maximum wind gust (m/s) - Windböe Maximum
|
|
40
|
+
"p_mittel": Parameter.PRES, # Mean air pressure (hPa) - Luftdruck Mittelwert
|
|
41
|
+
"rf_mittel": Parameter.RHUM, # Mean relative humidity (%) - Relative Feuchte Mittelwert
|
|
42
|
+
"so_h": Parameter.TSUN, # Sunshine duration (h) - Sonnenscheindauer
|
|
43
|
+
"bewm_mittel": Parameter.CLDC, # Mean cloud cover (%) - Bedeckung Mittelwert
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Inverse mapping
|
|
47
|
+
METEOSTAT_TO_GSA = {v: k for k, v in PARAMETER_MAPPING.items()}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@cache_service.cache(TTL.WEEK, "pickle")
|
|
51
|
+
def get_data(
|
|
52
|
+
station_id: str,
|
|
53
|
+
elevation: int | None,
|
|
54
|
+
parameters: list[str],
|
|
55
|
+
start: datetime,
|
|
56
|
+
end: datetime,
|
|
57
|
+
) -> Optional[pd.DataFrame]:
|
|
58
|
+
"""
|
|
59
|
+
Fetch data from GeoSphere Austria Data Hub API
|
|
60
|
+
"""
|
|
61
|
+
logger.debug(
|
|
62
|
+
f"Fetching daily data for station '{station_id}' from {start} to {end}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Format dates as ISO 8601 (date only for daily data)
|
|
66
|
+
start_str = start.strftime("%Y-%m-%d")
|
|
67
|
+
end_str = end.strftime("%Y-%m-%d")
|
|
68
|
+
|
|
69
|
+
# Build URL
|
|
70
|
+
url = f"{config.gsa_api_base_url}/station/historical/{RESOURCE_ID}"
|
|
71
|
+
|
|
72
|
+
# Make request
|
|
73
|
+
response = network_service.get(
|
|
74
|
+
url,
|
|
75
|
+
params={
|
|
76
|
+
"parameters": ",".join(parameters),
|
|
77
|
+
"station_ids": station_id,
|
|
78
|
+
"start": start_str,
|
|
79
|
+
"end": end_str,
|
|
80
|
+
"output_format": "geojson",
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if response.status_code != 200:
|
|
85
|
+
logger.warning(
|
|
86
|
+
f"Failed to fetch daily data for station {station_id} (status: {response.status_code})"
|
|
87
|
+
)
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
data = response.json()
|
|
92
|
+
|
|
93
|
+
if not data.get("features"):
|
|
94
|
+
logger.info(f"No daily data returned for station {station_id}")
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
# Get timestamps array
|
|
98
|
+
timestamps = data.get("timestamps")
|
|
99
|
+
if not timestamps:
|
|
100
|
+
logger.warning("No timestamps in daily response")
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
# Extract time series data from GeoJSON response
|
|
104
|
+
# New API format has timestamps at top level and data as arrays
|
|
105
|
+
feature = data["features"][0]
|
|
106
|
+
props = feature.get("properties", {})
|
|
107
|
+
params_data = props.get("parameters", {})
|
|
108
|
+
|
|
109
|
+
if not params_data:
|
|
110
|
+
logger.info(f"No parameter data returned for station {station_id}")
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
# Build DataFrame from timestamps and parameter arrays
|
|
114
|
+
df_dict = {}
|
|
115
|
+
for param in parameters:
|
|
116
|
+
if param in params_data:
|
|
117
|
+
param_info = params_data[param]
|
|
118
|
+
if "data" in param_info:
|
|
119
|
+
df_dict[param] = param_info["data"]
|
|
120
|
+
|
|
121
|
+
if not df_dict:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
# Create DataFrame with timestamps as index
|
|
125
|
+
df = pd.DataFrame(df_dict)
|
|
126
|
+
dt_index = pd.DatetimeIndex(pd.to_datetime(timestamps))
|
|
127
|
+
df.index = dt_index.tz_localize(None)
|
|
128
|
+
df.index.name = "time"
|
|
129
|
+
|
|
130
|
+
# Sort by time
|
|
131
|
+
df = df.sort_index()
|
|
132
|
+
|
|
133
|
+
# Rename columns to Meteostat parameter names
|
|
134
|
+
rename_map = {}
|
|
135
|
+
for gsadh_param, meteostat_param in PARAMETER_MAPPING.items():
|
|
136
|
+
if gsadh_param in df.columns:
|
|
137
|
+
rename_map[gsadh_param] = meteostat_param
|
|
138
|
+
|
|
139
|
+
df = df.rename(columns=rename_map)
|
|
140
|
+
|
|
141
|
+
# Convert units where necessary
|
|
142
|
+
if Parameter.WSPD in df.columns:
|
|
143
|
+
df[Parameter.WSPD] = df[Parameter.WSPD].apply(ms_to_kmh)
|
|
144
|
+
|
|
145
|
+
if Parameter.WPGT in df.columns:
|
|
146
|
+
df[Parameter.WPGT] = df[Parameter.WPGT].apply(ms_to_kmh)
|
|
147
|
+
|
|
148
|
+
if Parameter.TSUN in df.columns:
|
|
149
|
+
df[Parameter.TSUN] = df[Parameter.TSUN].apply(hours_to_minutes)
|
|
150
|
+
|
|
151
|
+
if Parameter.CLDC in df.columns:
|
|
152
|
+
df[Parameter.CLDC] = df[Parameter.CLDC].apply(percentage_to_okta)
|
|
153
|
+
|
|
154
|
+
if Parameter.PRES in df.columns:
|
|
155
|
+
df[Parameter.PRES] = df.apply(
|
|
156
|
+
lambda row: pres_to_msl(row, elevation), axis=1
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Round values
|
|
160
|
+
df = df.round(1)
|
|
161
|
+
|
|
162
|
+
return df
|
|
163
|
+
|
|
164
|
+
except Exception as error:
|
|
165
|
+
logger.warning(f"Error parsing daily response: {error}", exc_info=True)
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
|
|
170
|
+
"""
|
|
171
|
+
Fetch daily data from GeoSphere Austria Data Hub
|
|
172
|
+
"""
|
|
173
|
+
if "national" not in req.station.identifiers:
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
station_id = req.station.identifiers["national"]
|
|
177
|
+
|
|
178
|
+
# Map Meteostat parameters to GeoSphere Austria parameters
|
|
179
|
+
gsa_params = []
|
|
180
|
+
for param in req.parameters:
|
|
181
|
+
if param in METEOSTAT_TO_GSA:
|
|
182
|
+
gsa_params.append(METEOSTAT_TO_GSA[param])
|
|
183
|
+
|
|
184
|
+
if not gsa_params:
|
|
185
|
+
logger.info("No mappable parameters for GeoSphere Austria daily data")
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
# Fetch data
|
|
189
|
+
df = get_data(station_id, req.station.elevation, gsa_params, req.start, req.end)
|
|
190
|
+
|
|
191
|
+
if df is None or df.empty:
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
return df
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GeoSphere Austria Data Hub hourly data import routine
|
|
3
|
+
|
|
4
|
+
Get hourly climate data for weather stations in Austria.
|
|
5
|
+
|
|
6
|
+
License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Dict, Optional
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
from meteostat.enumerations import TTL, Parameter
|
|
15
|
+
from meteostat.core.logger import logger
|
|
16
|
+
from meteostat.typing import ProviderRequest
|
|
17
|
+
from meteostat.core.cache import cache_service
|
|
18
|
+
from meteostat.api.config import config
|
|
19
|
+
from meteostat.core.network import network_service
|
|
20
|
+
from meteostat.utils.conversions import hours_to_minutes, ms_to_kmh
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
RESOURCE_ID = "klima-v2-1h"
|
|
24
|
+
|
|
25
|
+
# Mapping from GeoSphere Austria parameter names to Meteostat parameters
|
|
26
|
+
# See: https://dataset.api.hub.geosphere.at/v1/station/historical/klima-v2-1h/metadata
|
|
27
|
+
PARAMETER_MAPPING: Dict[str, Parameter] = {
|
|
28
|
+
"tl": Parameter.TEMP, # Air temperature (°C)
|
|
29
|
+
"rr": Parameter.PRCP, # Precipitation (mm)
|
|
30
|
+
"pred": Parameter.PRES, # Air pressure (hPa)
|
|
31
|
+
"ff": Parameter.WSPD, # Wind speed (m/s)
|
|
32
|
+
"ffx": Parameter.WPGT, # Wind gust (m/s)
|
|
33
|
+
"dd": Parameter.WDIR, # Wind direction (°)
|
|
34
|
+
"rf": Parameter.RHUM, # Relative humidity (%)
|
|
35
|
+
"so_h": Parameter.TSUN, # Sunshine duration (h)
|
|
36
|
+
"sh": Parameter.SNWD, # Snow depth (cm)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Inverse mapping
|
|
40
|
+
METEOSTAT_TO_GSA = {v: k for k, v in PARAMETER_MAPPING.items()}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@cache_service.cache(TTL.DAY, "pickle")
|
|
44
|
+
def get_data(
|
|
45
|
+
station_id: str, parameters: list[str], start: datetime, end: datetime
|
|
46
|
+
) -> Optional[pd.DataFrame]:
|
|
47
|
+
"""
|
|
48
|
+
Fetch data from GeoSphere Austria Data Hub API
|
|
49
|
+
"""
|
|
50
|
+
logger.debug(
|
|
51
|
+
f"Fetching hourly data for station '{station_id}' from {start} to {end}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Format dates as ISO 8601
|
|
55
|
+
start_str = start.strftime("%Y-%m-%dT%H:%M")
|
|
56
|
+
end_str = end.strftime("%Y-%m-%dT%H:%M")
|
|
57
|
+
|
|
58
|
+
# Build URL
|
|
59
|
+
url = f"{config.gsa_api_base_url}/station/historical/{RESOURCE_ID}"
|
|
60
|
+
|
|
61
|
+
# Make request
|
|
62
|
+
response = network_service.get(
|
|
63
|
+
url,
|
|
64
|
+
params={
|
|
65
|
+
"parameters": ",".join(parameters),
|
|
66
|
+
"station_ids": station_id,
|
|
67
|
+
"start": start_str,
|
|
68
|
+
"end": end_str,
|
|
69
|
+
"output_format": "geojson",
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if response.status_code != 200:
|
|
74
|
+
logger.warning(
|
|
75
|
+
f"Failed to fetch data for station {station_id} (status: {response.status_code})"
|
|
76
|
+
)
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
data = response.json()
|
|
81
|
+
|
|
82
|
+
if not data.get("features"):
|
|
83
|
+
logger.info(f"No data returned for station {station_id}")
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
# Get timestamps array
|
|
87
|
+
timestamps = data.get("timestamps")
|
|
88
|
+
if not timestamps:
|
|
89
|
+
logger.warning("No timestamps in hourly response")
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
# Extract time series data from GeoJSON response
|
|
93
|
+
# New API format has timestamps at top level and data as arrays
|
|
94
|
+
feature = data["features"][0]
|
|
95
|
+
props = feature.get("properties", {})
|
|
96
|
+
params_data = props.get("parameters", {})
|
|
97
|
+
|
|
98
|
+
if not params_data:
|
|
99
|
+
logger.info(f"No parameter data returned for station {station_id}")
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
# Build DataFrame from timestamps and parameter arrays
|
|
103
|
+
df_dict = {}
|
|
104
|
+
for param in parameters:
|
|
105
|
+
if param in params_data:
|
|
106
|
+
param_info = params_data[param]
|
|
107
|
+
if "data" in param_info:
|
|
108
|
+
df_dict[param] = param_info["data"]
|
|
109
|
+
|
|
110
|
+
if not df_dict:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
# Create DataFrame with timestamps as index
|
|
114
|
+
df = pd.DataFrame(df_dict)
|
|
115
|
+
dt_index = pd.DatetimeIndex(pd.to_datetime(timestamps))
|
|
116
|
+
df.index = dt_index.tz_localize(None)
|
|
117
|
+
df.index.name = "time"
|
|
118
|
+
|
|
119
|
+
# Sort by time
|
|
120
|
+
df = df.sort_index()
|
|
121
|
+
|
|
122
|
+
# Rename columns to Meteostat parameter names
|
|
123
|
+
rename_map = {}
|
|
124
|
+
for gsadh_param, meteostat_param in PARAMETER_MAPPING.items():
|
|
125
|
+
if gsadh_param in df.columns:
|
|
126
|
+
rename_map[gsadh_param] = meteostat_param
|
|
127
|
+
|
|
128
|
+
df = df.rename(columns=rename_map)
|
|
129
|
+
|
|
130
|
+
# Convert units where necessary
|
|
131
|
+
if Parameter.WSPD in df.columns:
|
|
132
|
+
df[Parameter.WSPD] = df[Parameter.WSPD].apply(ms_to_kmh)
|
|
133
|
+
|
|
134
|
+
if Parameter.WPGT in df.columns:
|
|
135
|
+
df[Parameter.WPGT] = df[Parameter.WPGT].apply(ms_to_kmh)
|
|
136
|
+
|
|
137
|
+
if Parameter.TSUN in df.columns:
|
|
138
|
+
df[Parameter.TSUN] = df[Parameter.TSUN].apply(hours_to_minutes)
|
|
139
|
+
|
|
140
|
+
# Round values
|
|
141
|
+
df = df.round(1)
|
|
142
|
+
|
|
143
|
+
return df
|
|
144
|
+
|
|
145
|
+
except Exception as error:
|
|
146
|
+
logger.warning(f"Error parsing response: {error}", exc_info=True)
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
|
|
151
|
+
"""
|
|
152
|
+
Fetch hourly data from GeoSphere Austria Data Hub
|
|
153
|
+
"""
|
|
154
|
+
if "national" not in req.station.identifiers:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
station_id = req.station.identifiers["national"]
|
|
158
|
+
|
|
159
|
+
# Map Meteostat parameters to GeoSphere Austria parameters
|
|
160
|
+
gsa_params = []
|
|
161
|
+
for param in req.parameters:
|
|
162
|
+
if param in METEOSTAT_TO_GSA:
|
|
163
|
+
gsa_params.append(METEOSTAT_TO_GSA[param])
|
|
164
|
+
|
|
165
|
+
if not gsa_params:
|
|
166
|
+
logger.info("No mappable parameters for GeoSphere Austria hourly data")
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
# Fetch data
|
|
170
|
+
df = get_data(station_id, gsa_params, req.start, req.end)
|
|
171
|
+
|
|
172
|
+
if df is None or df.empty:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
return df
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GeoSphere Austria Data Hub monthly data import routine
|
|
3
|
+
|
|
4
|
+
Get monthly climate data for weather stations in Austria.
|
|
5
|
+
|
|
6
|
+
License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Dict, Optional
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
from meteostat.enumerations import TTL, Parameter
|
|
15
|
+
from meteostat.core.logger import logger
|
|
16
|
+
from meteostat.typing import ProviderRequest
|
|
17
|
+
from meteostat.core.cache import cache_service
|
|
18
|
+
from meteostat.api.config import config
|
|
19
|
+
from meteostat.core.network import network_service
|
|
20
|
+
from meteostat.utils.conversions import (
|
|
21
|
+
hours_to_minutes,
|
|
22
|
+
ms_to_kmh,
|
|
23
|
+
percentage_to_okta,
|
|
24
|
+
pres_to_msl,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
RESOURCE_ID = "klima-v2-1m"
|
|
29
|
+
|
|
30
|
+
# Mapping from GeoSphere Austria parameter names to Meteostat parameters
|
|
31
|
+
# See: https://dataset.api.hub.geosphere.at/v1/station/historical/klima-v2-1m/metadata
|
|
32
|
+
PARAMETER_MAPPING: Dict[str, Parameter] = {
|
|
33
|
+
"tl_mittel": Parameter.TEMP, # Mean air temperature (°C) - Lufttemperatur 2m Mittelwert
|
|
34
|
+
"tlmin": Parameter.TXMN, # Minimum air temperature (°C) - Lufttemperatur 2m Minimalwert
|
|
35
|
+
"tlmax": Parameter.TXMX, # Maximum air temperature (°C) - Lufttemperatur 2m Maximalwert
|
|
36
|
+
"tlmin_mittel": Parameter.TMIN, # Mean minimum air temperature (°C) - Lufttemperatur 2m Mittelwert der Minima
|
|
37
|
+
"tlmax_mittel": Parameter.TMAX, # Mean maximum air temperature (°C) - Lufttemperatur 2m Mittelwert der Maxima
|
|
38
|
+
"rf_mittel": Parameter.RHUM, # Mean relative humidity (%) - Relative Feuchte Mittelwert
|
|
39
|
+
"rr": Parameter.PRCP, # Precipitation sum (mm) - Niederschlag Summe der 24h-Summen
|
|
40
|
+
"vv_mittel": Parameter.WSPD, # Mean wind speed (m/s) - Windgeschwindigkeit Mittelwert
|
|
41
|
+
"p": Parameter.PRES, # Mean air pressure (hPa) - Luftdruck Mittelwert
|
|
42
|
+
"so_h": Parameter.TSUN, # Sunshine duration (h) - Sonnenscheindauer
|
|
43
|
+
"bewm_mittel": Parameter.CLDC, # Mean cloud cover (%) - Bedeckung Mittelwert
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Inverse mapping
|
|
47
|
+
METEOSTAT_TO_GSA = {v: k for k, v in PARAMETER_MAPPING.items()}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@cache_service.cache(TTL.MONTH, "pickle")
|
|
51
|
+
def get_data(
|
|
52
|
+
station_id: str,
|
|
53
|
+
elevation: int | None,
|
|
54
|
+
parameters: list[str],
|
|
55
|
+
start: datetime,
|
|
56
|
+
end: datetime,
|
|
57
|
+
) -> Optional[pd.DataFrame]:
|
|
58
|
+
"""
|
|
59
|
+
Fetch data from GeoSphere Austria Data Hub API
|
|
60
|
+
"""
|
|
61
|
+
logger.debug(
|
|
62
|
+
f"Fetching monthly data for station '{station_id}' from {start} to {end}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Format dates as ISO 8601 (full date for monthly data)
|
|
66
|
+
start_str = start.strftime("%Y-%m-%d")
|
|
67
|
+
end_str = end.strftime("%Y-%m-%d")
|
|
68
|
+
|
|
69
|
+
# Build URL
|
|
70
|
+
url = f"{config.gsa_api_base_url}/station/historical/{RESOURCE_ID}"
|
|
71
|
+
|
|
72
|
+
# Make request
|
|
73
|
+
response = network_service.get(
|
|
74
|
+
url,
|
|
75
|
+
params={
|
|
76
|
+
"parameters": ",".join(parameters),
|
|
77
|
+
"station_ids": station_id,
|
|
78
|
+
"start": start_str,
|
|
79
|
+
"end": end_str,
|
|
80
|
+
"output_format": "geojson",
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if response.status_code != 200:
|
|
85
|
+
logger.warning(
|
|
86
|
+
f"Failed to fetch monthly data for station {station_id} (status: {response.status_code}): {response.json()}",
|
|
87
|
+
)
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
data = response.json()
|
|
92
|
+
|
|
93
|
+
if not data.get("features"):
|
|
94
|
+
logger.info(f"No monthly data returned for station {station_id}")
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
# Get timestamps array
|
|
98
|
+
timestamps = data.get("timestamps")
|
|
99
|
+
if not timestamps:
|
|
100
|
+
logger.warning("No timestamps in monthly response")
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
# Extract time series data from GeoJSON response
|
|
104
|
+
# New API format has timestamps at top level and data as arrays
|
|
105
|
+
feature = data["features"][0]
|
|
106
|
+
props = feature.get("properties", {})
|
|
107
|
+
params_data = props.get("parameters", {})
|
|
108
|
+
|
|
109
|
+
if not params_data:
|
|
110
|
+
logger.info(f"No parameter data returned for station {station_id}")
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
# Build DataFrame from timestamps and parameter arrays
|
|
114
|
+
df_dict = {}
|
|
115
|
+
for param in parameters:
|
|
116
|
+
if param in params_data:
|
|
117
|
+
param_info = params_data[param]
|
|
118
|
+
if "data" in param_info:
|
|
119
|
+
df_dict[param] = param_info["data"]
|
|
120
|
+
|
|
121
|
+
if not df_dict:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
# Create DataFrame with timestamps as index
|
|
125
|
+
df = pd.DataFrame(df_dict)
|
|
126
|
+
dt_index = pd.DatetimeIndex(pd.to_datetime(timestamps))
|
|
127
|
+
df.index = dt_index.tz_localize(None)
|
|
128
|
+
df.index.name = "time"
|
|
129
|
+
|
|
130
|
+
# Sort by time
|
|
131
|
+
df = df.sort_index()
|
|
132
|
+
|
|
133
|
+
# Rename columns to Meteostat parameter names
|
|
134
|
+
rename_map = {}
|
|
135
|
+
for gsadh_param, meteostat_param in PARAMETER_MAPPING.items():
|
|
136
|
+
if gsadh_param in df.columns:
|
|
137
|
+
rename_map[gsadh_param] = meteostat_param
|
|
138
|
+
|
|
139
|
+
df = df.rename(columns=rename_map)
|
|
140
|
+
|
|
141
|
+
# Convert units where necessary
|
|
142
|
+
if Parameter.WSPD in df.columns:
|
|
143
|
+
df[Parameter.WSPD] = df[Parameter.WSPD].apply(ms_to_kmh)
|
|
144
|
+
|
|
145
|
+
if Parameter.TSUN in df.columns:
|
|
146
|
+
df[Parameter.TSUN] = df[Parameter.TSUN].apply(hours_to_minutes)
|
|
147
|
+
|
|
148
|
+
if Parameter.CLDC in df.columns:
|
|
149
|
+
# Convert cloud cover from % to okta
|
|
150
|
+
df[Parameter.CLDC] = df[Parameter.CLDC].apply(percentage_to_okta)
|
|
151
|
+
|
|
152
|
+
if Parameter.PRES in df.columns:
|
|
153
|
+
df[Parameter.PRES] = df.apply(
|
|
154
|
+
lambda row: pres_to_msl(row, elevation), axis=1
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Round values
|
|
158
|
+
df = df.round(1)
|
|
159
|
+
|
|
160
|
+
return df
|
|
161
|
+
|
|
162
|
+
except Exception as error:
|
|
163
|
+
logger.warning(f"Error parsing monthly response: {error}", exc_info=True)
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
|
|
168
|
+
"""
|
|
169
|
+
Fetch monthly data from GeoSphere Austria Data Hub
|
|
170
|
+
"""
|
|
171
|
+
if "national" not in req.station.identifiers:
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
station_id = req.station.identifiers["national"]
|
|
175
|
+
|
|
176
|
+
# Map Meteostat parameters to GeoSphere Austria parameters
|
|
177
|
+
gsa_params = []
|
|
178
|
+
for param in req.parameters:
|
|
179
|
+
if param in METEOSTAT_TO_GSA:
|
|
180
|
+
gsa_params.append(METEOSTAT_TO_GSA[param])
|
|
181
|
+
|
|
182
|
+
if not gsa_params:
|
|
183
|
+
logger.info("No mappable parameters for GeoSphere Austria monthly data")
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
# Fetch data
|
|
187
|
+
df = get_data(station_id, req.station.elevation, gsa_params, req.start, req.end)
|
|
188
|
+
|
|
189
|
+
if df is None or df.empty:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
return df
|