meteostat 1.7.6__py3-none-any.whl → 2.0.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 +32 -19
- meteostat/api/daily.py +76 -0
- meteostat/api/hourly.py +80 -0
- meteostat/api/interpolate.py +240 -0
- meteostat/api/inventory.py +59 -0
- meteostat/api/merge.py +103 -0
- meteostat/api/monthly.py +73 -0
- meteostat/api/normals.py +144 -0
- meteostat/api/point.py +30 -0
- meteostat/api/stations.py +234 -0
- meteostat/api/timeseries.py +334 -0
- meteostat/core/cache.py +212 -59
- meteostat/core/config.py +158 -0
- meteostat/core/data.py +199 -0
- meteostat/core/logger.py +9 -0
- meteostat/core/network.py +82 -0
- meteostat/core/parameters.py +112 -0
- meteostat/core/providers.py +184 -0
- meteostat/core/schema.py +170 -0
- meteostat/core/validator.py +38 -0
- meteostat/enumerations.py +149 -0
- meteostat/interpolation/idw.py +120 -0
- meteostat/interpolation/lapserate.py +91 -0
- meteostat/interpolation/nearest.py +31 -0
- meteostat/parameters.py +354 -0
- meteostat/providers/dwd/climat.py +166 -0
- meteostat/providers/dwd/daily.py +144 -0
- meteostat/providers/dwd/hourly.py +218 -0
- meteostat/providers/dwd/monthly.py +138 -0
- meteostat/providers/dwd/mosmix.py +351 -0
- meteostat/providers/dwd/poi.py +117 -0
- meteostat/providers/dwd/shared.py +155 -0
- meteostat/providers/eccc/daily.py +87 -0
- meteostat/providers/eccc/hourly.py +104 -0
- meteostat/providers/eccc/monthly.py +66 -0
- meteostat/providers/eccc/shared.py +45 -0
- meteostat/providers/index.py +496 -0
- meteostat/providers/meteostat/daily.py +65 -0
- meteostat/providers/meteostat/daily_derived.py +110 -0
- meteostat/providers/meteostat/hourly.py +66 -0
- meteostat/providers/meteostat/monthly.py +45 -0
- meteostat/providers/meteostat/monthly_derived.py +106 -0
- meteostat/providers/meteostat/shared.py +93 -0
- meteostat/providers/metno/forecast.py +186 -0
- meteostat/providers/noaa/ghcnd.py +228 -0
- meteostat/providers/noaa/isd_lite.py +142 -0
- meteostat/providers/noaa/metar.py +163 -0
- meteostat/typing.py +113 -0
- meteostat/utils/conversions.py +231 -0
- meteostat/utils/data.py +194 -0
- meteostat/utils/geo.py +28 -0
- meteostat/utils/parsers.py +168 -0
- meteostat/utils/types.py +113 -0
- meteostat/utils/validators.py +31 -0
- meteostat-2.0.0.dist-info/METADATA +134 -0
- meteostat-2.0.0.dist-info/RECORD +63 -0
- {meteostat-1.7.6.dist-info → meteostat-2.0.0.dist-info}/WHEEL +1 -2
- meteostat/core/loader.py +0 -103
- meteostat/core/warn.py +0 -34
- meteostat/enumerations/granularity.py +0 -22
- meteostat/interface/base.py +0 -39
- meteostat/interface/daily.py +0 -118
- meteostat/interface/hourly.py +0 -154
- meteostat/interface/meteodata.py +0 -210
- meteostat/interface/monthly.py +0 -109
- meteostat/interface/normals.py +0 -245
- meteostat/interface/point.py +0 -143
- meteostat/interface/stations.py +0 -252
- meteostat/interface/timeseries.py +0 -237
- meteostat/series/aggregate.py +0 -48
- meteostat/series/convert.py +0 -28
- meteostat/series/count.py +0 -17
- meteostat/series/coverage.py +0 -20
- meteostat/series/fetch.py +0 -28
- meteostat/series/interpolate.py +0 -47
- meteostat/series/normalize.py +0 -76
- meteostat/series/stations.py +0 -22
- meteostat/units.py +0 -149
- meteostat/utilities/__init__.py +0 -0
- meteostat/utilities/aggregations.py +0 -37
- meteostat/utilities/endpoint.py +0 -33
- meteostat/utilities/helpers.py +0 -70
- meteostat/utilities/mutations.py +0 -89
- meteostat/utilities/validations.py +0 -30
- meteostat-1.7.6.dist-info/METADATA +0 -112
- meteostat-1.7.6.dist-info/RECORD +0 -39
- meteostat-1.7.6.dist-info/top_level.txt +0 -1
- /meteostat/{core → api}/__init__.py +0 -0
- /meteostat/{enumerations → interpolation}/__init__.py +0 -0
- /meteostat/{interface → providers}/__init__.py +0 -0
- /meteostat/{interface/interpolate.py → py.typed} +0 -0
- /meteostat/{series → utils}/__init__.py +0 -0
- {meteostat-1.7.6.dist-info → meteostat-2.0.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DWD MOSMIX data provider
|
|
3
|
+
|
|
4
|
+
Parameters: https://www.dwd.de/DE/leistungen/met_verfahren_mosmix/mosmix_parameteruebersicht.pdf?__blob=publicationFile&v=3
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from io import BytesIO
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from zipfile import ZipFile
|
|
12
|
+
from lxml import etree # type: ignore
|
|
13
|
+
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
16
|
+
from meteostat.core.cache import cache_service
|
|
17
|
+
from meteostat.enumerations import TTL, Parameter
|
|
18
|
+
from meteostat.typing import ProviderRequest
|
|
19
|
+
from meteostat.utils.conversions import (
|
|
20
|
+
kelvin_to_celsius,
|
|
21
|
+
ms_to_kmh,
|
|
22
|
+
percentage_to_okta,
|
|
23
|
+
temp_dwpt_to_rhum,
|
|
24
|
+
)
|
|
25
|
+
from meteostat.core.network import network_service
|
|
26
|
+
|
|
27
|
+
ENDPOINT = "https://opendata.dwd.de/weather/local_forecasts/mos/MOSMIX_L/single_stations/{station}/kml/MOSMIX_L_LATEST_{station}.kmz"
|
|
28
|
+
COCO_MAP = {
|
|
29
|
+
"0": 1,
|
|
30
|
+
"1": 2,
|
|
31
|
+
"2": 3,
|
|
32
|
+
"3": 4,
|
|
33
|
+
"45": 5,
|
|
34
|
+
"49": 5,
|
|
35
|
+
"61": 7,
|
|
36
|
+
"63": 8,
|
|
37
|
+
"65": 9,
|
|
38
|
+
"51": 7,
|
|
39
|
+
"53": 8,
|
|
40
|
+
"55": 9,
|
|
41
|
+
"68": 12,
|
|
42
|
+
"69": 13,
|
|
43
|
+
"71": 14,
|
|
44
|
+
"73": 15,
|
|
45
|
+
"75": 16,
|
|
46
|
+
"80": 17,
|
|
47
|
+
"81": 18,
|
|
48
|
+
"82": 18,
|
|
49
|
+
"83": 19,
|
|
50
|
+
"84": 20,
|
|
51
|
+
"85": 21,
|
|
52
|
+
"86": 22,
|
|
53
|
+
"66": 10,
|
|
54
|
+
"67": 11,
|
|
55
|
+
"56": 10,
|
|
56
|
+
"57": 11,
|
|
57
|
+
"95": 25,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_coco(code: str | int) -> Optional[int]:
|
|
62
|
+
"""
|
|
63
|
+
Map DWD MOSMIX weather condition codes to Meteostat condicodes
|
|
64
|
+
"""
|
|
65
|
+
return COCO_MAP.get(str(code))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@cache_service.cache(TTL.HOUR, "pickle")
|
|
69
|
+
def get_df(station: str) -> Optional[pd.DataFrame]:
|
|
70
|
+
# Fetch the KMZ file data in memory
|
|
71
|
+
response = network_service.get(ENDPOINT.format(station=station))
|
|
72
|
+
kmz_data = BytesIO(response.content)
|
|
73
|
+
|
|
74
|
+
# KMZ -> KML in memory
|
|
75
|
+
with ZipFile(kmz_data, "r") as kmz:
|
|
76
|
+
with kmz.open(kmz.infolist()[0].filename, "r") as raw:
|
|
77
|
+
kml = raw.read()
|
|
78
|
+
|
|
79
|
+
# Parse KML
|
|
80
|
+
tree = etree.fromstring(kml)
|
|
81
|
+
|
|
82
|
+
# Skip stale forecasts
|
|
83
|
+
issue_time = datetime.strptime(
|
|
84
|
+
tree.xpath(
|
|
85
|
+
"//kml:kml/kml:Document/kml:ExtendedData/"
|
|
86
|
+
+ "dwd:ProductDefinition/dwd:IssueTime",
|
|
87
|
+
namespaces=tree.nsmap,
|
|
88
|
+
)[0].text,
|
|
89
|
+
"%Y-%m-%dT%H:%M:%S.%fZ",
|
|
90
|
+
)
|
|
91
|
+
if (datetime.now() - issue_time).total_seconds() > 25200:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
# Collect all time steps
|
|
95
|
+
timesteps = []
|
|
96
|
+
for step in tree.xpath(
|
|
97
|
+
"//kml:kml/kml:Document/kml:ExtendedData/dwd:ProductDefinition/"
|
|
98
|
+
+ "dwd:ForecastTimeSteps/dwd:TimeStep",
|
|
99
|
+
namespaces=tree.nsmap,
|
|
100
|
+
):
|
|
101
|
+
timesteps.append(step.text)
|
|
102
|
+
|
|
103
|
+
# COLLECT WEATHER DATA
|
|
104
|
+
# Each parameter is processed individually
|
|
105
|
+
data = {
|
|
106
|
+
"time": timesteps,
|
|
107
|
+
Parameter.TEMP: [],
|
|
108
|
+
Parameter.DWPT: [],
|
|
109
|
+
Parameter.PRCP: [],
|
|
110
|
+
Parameter.WDIR: [],
|
|
111
|
+
Parameter.WSPD: [],
|
|
112
|
+
Parameter.WPGT: [],
|
|
113
|
+
Parameter.TSUN: [],
|
|
114
|
+
Parameter.PRES: [],
|
|
115
|
+
Parameter.CLDC: [],
|
|
116
|
+
Parameter.VSBY: [],
|
|
117
|
+
Parameter.COCO: [],
|
|
118
|
+
}
|
|
119
|
+
placemark = tree.xpath(
|
|
120
|
+
"//kml:kml/kml:Document/kml:Placemark", namespaces=tree.nsmap
|
|
121
|
+
)[0]
|
|
122
|
+
|
|
123
|
+
# Pressure
|
|
124
|
+
for value in (
|
|
125
|
+
re.sub(
|
|
126
|
+
r"/\s+/",
|
|
127
|
+
" ",
|
|
128
|
+
placemark.xpath(
|
|
129
|
+
'kml:ExtendedData/dwd:Forecast[@dwd:elementName="PPPP"]/dwd:value',
|
|
130
|
+
namespaces=tree.nsmap,
|
|
131
|
+
)[0].text,
|
|
132
|
+
)
|
|
133
|
+
.strip()
|
|
134
|
+
.split()
|
|
135
|
+
):
|
|
136
|
+
data[Parameter.PRES].append(
|
|
137
|
+
float(value) / 100
|
|
138
|
+
if value.lstrip("-").replace(".", "", 1).isdigit()
|
|
139
|
+
else None
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Air temperature
|
|
143
|
+
for value in (
|
|
144
|
+
re.sub(
|
|
145
|
+
r"/\s+/",
|
|
146
|
+
" ",
|
|
147
|
+
placemark.xpath(
|
|
148
|
+
'kml:ExtendedData/dwd:Forecast[@dwd:elementName="TTT"]/dwd:value',
|
|
149
|
+
namespaces=tree.nsmap,
|
|
150
|
+
)[0].text,
|
|
151
|
+
)
|
|
152
|
+
.strip()
|
|
153
|
+
.split()
|
|
154
|
+
):
|
|
155
|
+
data[Parameter.TEMP].append(
|
|
156
|
+
kelvin_to_celsius(float(value))
|
|
157
|
+
if value.lstrip("-").replace(".", "", 1).isdigit()
|
|
158
|
+
else None
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Dew point
|
|
162
|
+
for value in (
|
|
163
|
+
re.sub(
|
|
164
|
+
r"/\s+/",
|
|
165
|
+
" ",
|
|
166
|
+
placemark.xpath(
|
|
167
|
+
'kml:ExtendedData/dwd:Forecast[@dwd:elementName="Td"]/dwd:value',
|
|
168
|
+
namespaces=tree.nsmap,
|
|
169
|
+
)[0].text,
|
|
170
|
+
)
|
|
171
|
+
.strip()
|
|
172
|
+
.split()
|
|
173
|
+
):
|
|
174
|
+
data[Parameter.DWPT].append(
|
|
175
|
+
kelvin_to_celsius(float(value))
|
|
176
|
+
if value.lstrip("-").replace(".", "", 1).isdigit()
|
|
177
|
+
else None
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Wind direction
|
|
181
|
+
for value in (
|
|
182
|
+
re.sub(
|
|
183
|
+
r"/\s+/",
|
|
184
|
+
" ",
|
|
185
|
+
placemark.xpath(
|
|
186
|
+
'kml:ExtendedData/dwd:Forecast[@dwd:elementName="DD"]/dwd:value',
|
|
187
|
+
namespaces=tree.nsmap,
|
|
188
|
+
)[0].text,
|
|
189
|
+
)
|
|
190
|
+
.strip()
|
|
191
|
+
.split()
|
|
192
|
+
):
|
|
193
|
+
data[Parameter.WDIR].append(
|
|
194
|
+
int(float(value))
|
|
195
|
+
if value.lstrip("-").replace(".", "", 1).isdigit()
|
|
196
|
+
else None
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Wind speed
|
|
200
|
+
for value in (
|
|
201
|
+
re.sub(
|
|
202
|
+
r"/\s+/",
|
|
203
|
+
" ",
|
|
204
|
+
placemark.xpath(
|
|
205
|
+
'kml:ExtendedData/dwd:Forecast[@dwd:elementName="FF"]/dwd:value',
|
|
206
|
+
namespaces=tree.nsmap,
|
|
207
|
+
)[0].text,
|
|
208
|
+
)
|
|
209
|
+
.strip()
|
|
210
|
+
.split()
|
|
211
|
+
):
|
|
212
|
+
data[Parameter.WSPD].append(
|
|
213
|
+
ms_to_kmh(float(value))
|
|
214
|
+
if value.lstrip("-").replace(".", "", 1).isdigit()
|
|
215
|
+
else None
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Peak wind gust
|
|
219
|
+
for value in (
|
|
220
|
+
re.sub(
|
|
221
|
+
r"/\s+/",
|
|
222
|
+
" ",
|
|
223
|
+
placemark.xpath(
|
|
224
|
+
'kml:ExtendedData/dwd:Forecast[@dwd:elementName="FX1"]/dwd:value',
|
|
225
|
+
namespaces=tree.nsmap,
|
|
226
|
+
)[0].text,
|
|
227
|
+
)
|
|
228
|
+
.strip()
|
|
229
|
+
.split()
|
|
230
|
+
):
|
|
231
|
+
data[Parameter.WPGT].append(
|
|
232
|
+
ms_to_kmh(float(value))
|
|
233
|
+
if value.lstrip("-").replace(".", "", 1).isdigit()
|
|
234
|
+
else None
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Weather condition
|
|
238
|
+
for value in (
|
|
239
|
+
re.sub(
|
|
240
|
+
r"/\s+/",
|
|
241
|
+
" ",
|
|
242
|
+
placemark.xpath(
|
|
243
|
+
'kml:ExtendedData/dwd:Forecast[@dwd:elementName="ww"]/dwd:value',
|
|
244
|
+
namespaces=tree.nsmap,
|
|
245
|
+
)[0].text,
|
|
246
|
+
)
|
|
247
|
+
.strip()
|
|
248
|
+
.split()
|
|
249
|
+
):
|
|
250
|
+
data[Parameter.COCO].append(
|
|
251
|
+
get_coco(int(float(value)))
|
|
252
|
+
if value.lstrip("-").replace(".", "", 1).isdigit()
|
|
253
|
+
else None
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Precipitation
|
|
257
|
+
for value in (
|
|
258
|
+
re.sub(
|
|
259
|
+
r"/\s+/",
|
|
260
|
+
" ",
|
|
261
|
+
placemark.xpath(
|
|
262
|
+
'kml:ExtendedData/dwd:Forecast[@dwd:elementName="RR1c"]/dwd:value',
|
|
263
|
+
namespaces=tree.nsmap,
|
|
264
|
+
)[0].text,
|
|
265
|
+
)
|
|
266
|
+
.strip()
|
|
267
|
+
.split()
|
|
268
|
+
):
|
|
269
|
+
data[Parameter.PRCP].append(
|
|
270
|
+
float(value) if value.lstrip("-").replace(".", "", 1).isdigit() else None
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Sunshine Duration
|
|
274
|
+
for value in (
|
|
275
|
+
re.sub(
|
|
276
|
+
r"/\s+/",
|
|
277
|
+
" ",
|
|
278
|
+
placemark.xpath(
|
|
279
|
+
'kml:ExtendedData/dwd:Forecast[@dwd:elementName="SunD1"]/dwd:value',
|
|
280
|
+
namespaces=tree.nsmap,
|
|
281
|
+
)[0].text,
|
|
282
|
+
)
|
|
283
|
+
.strip()
|
|
284
|
+
.split()
|
|
285
|
+
):
|
|
286
|
+
data[Parameter.TSUN].append(
|
|
287
|
+
float(value) / 60
|
|
288
|
+
if value.lstrip("-").replace(".", "", 1).isdigit()
|
|
289
|
+
else None
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Cloud Cover
|
|
293
|
+
for value in (
|
|
294
|
+
re.sub(
|
|
295
|
+
r"/\s+/",
|
|
296
|
+
" ",
|
|
297
|
+
placemark.xpath(
|
|
298
|
+
'kml:ExtendedData/dwd:Forecast[@dwd:elementName="N"]/dwd:value',
|
|
299
|
+
namespaces=tree.nsmap,
|
|
300
|
+
)[0].text,
|
|
301
|
+
)
|
|
302
|
+
.strip()
|
|
303
|
+
.split()
|
|
304
|
+
):
|
|
305
|
+
data[Parameter.CLDC].append(
|
|
306
|
+
percentage_to_okta(float(value))
|
|
307
|
+
if value.lstrip("-").replace(".", "", 1).isdigit()
|
|
308
|
+
else None
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Visibility
|
|
312
|
+
for value in (
|
|
313
|
+
re.sub(
|
|
314
|
+
r"/\s+/",
|
|
315
|
+
" ",
|
|
316
|
+
placemark.xpath(
|
|
317
|
+
'kml:ExtendedData/dwd:Forecast[@dwd:elementName="VV"]/dwd:value',
|
|
318
|
+
namespaces=tree.nsmap,
|
|
319
|
+
)[0].text,
|
|
320
|
+
)
|
|
321
|
+
.strip()
|
|
322
|
+
.split()
|
|
323
|
+
):
|
|
324
|
+
data[Parameter.VSBY].append(
|
|
325
|
+
float(value) if value.lstrip("-").replace(".", "", 1).isdigit() else None
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Convert data dict to DataFrame
|
|
329
|
+
df = pd.DataFrame.from_dict(data)
|
|
330
|
+
|
|
331
|
+
# Convert time strings to datetime
|
|
332
|
+
df["time"] = pd.to_datetime(df["time"])
|
|
333
|
+
|
|
334
|
+
# Calculate humidity data
|
|
335
|
+
df[Parameter.RHUM] = df.apply(temp_dwpt_to_rhum, axis=1)
|
|
336
|
+
|
|
337
|
+
# Set index
|
|
338
|
+
df = df.set_index(["time"])
|
|
339
|
+
|
|
340
|
+
# Round decimals
|
|
341
|
+
df = df.round(1)
|
|
342
|
+
|
|
343
|
+
# Remove tz awareness
|
|
344
|
+
df = df.tz_convert(None, level="time")
|
|
345
|
+
|
|
346
|
+
return df
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
|
|
350
|
+
if "mosmix" in req.station.identifiers:
|
|
351
|
+
return get_df(req.station.identifiers["mosmix"])
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from typing import Optional, Union
|
|
2
|
+
from urllib.error import HTTPError
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from meteostat.core.cache import cache_service
|
|
7
|
+
from meteostat.core.logger import logger
|
|
8
|
+
from meteostat.enumerations import TTL, Parameter
|
|
9
|
+
from meteostat.typing import ProviderRequest
|
|
10
|
+
from meteostat.utils.conversions import percentage_to_okta
|
|
11
|
+
|
|
12
|
+
ENDPOINT = "https://opendata.dwd.de/weather/weather_reports/poi/{station}-BEOB.csv"
|
|
13
|
+
# TODO: add col 11 for solar radiation support (Globalstrahlung)
|
|
14
|
+
USECOLS = [0, 1, 2, 9, 14, 21, 22, 23, 33, 35, 36, 37, 40, 41]
|
|
15
|
+
NAMES = {
|
|
16
|
+
"Wolkenbedeckung": Parameter.CLDC,
|
|
17
|
+
"Temperatur (2m)": Parameter.TEMP,
|
|
18
|
+
"Sichtweite": Parameter.VSBY,
|
|
19
|
+
"Windgeschwindigkeit": Parameter.WSPD,
|
|
20
|
+
"Windboen (letzte Stunde)": Parameter.WPGT,
|
|
21
|
+
"Niederschlag (letzte Stunde)": Parameter.PRCP,
|
|
22
|
+
"Relative Feuchte": Parameter.RHUM,
|
|
23
|
+
"Windrichtung": Parameter.WDIR,
|
|
24
|
+
"Druck (auf Meereshoehe)": Parameter.PRES,
|
|
25
|
+
"Sonnenscheindauer (letzte Stunde)": Parameter.TSUN,
|
|
26
|
+
"aktuelles Wetter": Parameter.COCO,
|
|
27
|
+
"Schneehoehe": Parameter.SNWD,
|
|
28
|
+
}
|
|
29
|
+
COCO_MAP = {
|
|
30
|
+
"1": 1,
|
|
31
|
+
"2": 2,
|
|
32
|
+
"3": 3,
|
|
33
|
+
"4": 4,
|
|
34
|
+
"5": 5,
|
|
35
|
+
"6": 6,
|
|
36
|
+
"7": 7,
|
|
37
|
+
"8": 8,
|
|
38
|
+
"9": 9,
|
|
39
|
+
"10": 10,
|
|
40
|
+
"11": 11,
|
|
41
|
+
"12": 12,
|
|
42
|
+
"13": 13,
|
|
43
|
+
"14": 14,
|
|
44
|
+
"15": 15,
|
|
45
|
+
"16": 16,
|
|
46
|
+
"17": 24,
|
|
47
|
+
"18": 17,
|
|
48
|
+
"19": 18,
|
|
49
|
+
"20": 19,
|
|
50
|
+
"21": 20,
|
|
51
|
+
"22": 21,
|
|
52
|
+
"23": 22,
|
|
53
|
+
"24": 19,
|
|
54
|
+
"25": 20,
|
|
55
|
+
"26": 23,
|
|
56
|
+
"27": 25,
|
|
57
|
+
"28": 26,
|
|
58
|
+
"29": 25,
|
|
59
|
+
"30": 26,
|
|
60
|
+
"31": 27,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_coco(code: str | int) -> Union[int, None]:
|
|
65
|
+
"""
|
|
66
|
+
Map DWD POI weather condition codes to Meteostat condicodes
|
|
67
|
+
"""
|
|
68
|
+
return COCO_MAP.get(str(code))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@cache_service.cache(TTL.HOUR, "pickle")
|
|
72
|
+
def get_df(station: str) -> Optional[pd.DataFrame]:
|
|
73
|
+
try:
|
|
74
|
+
# Read CSV data from DWD server
|
|
75
|
+
df = pd.read_csv( # type: ignore
|
|
76
|
+
ENDPOINT.format(station=station),
|
|
77
|
+
sep=";",
|
|
78
|
+
skiprows=2,
|
|
79
|
+
na_values="---",
|
|
80
|
+
usecols=USECOLS,
|
|
81
|
+
decimal=",",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Rename columns
|
|
85
|
+
df = df.rename(columns=NAMES)
|
|
86
|
+
|
|
87
|
+
# Snow cm -> mm
|
|
88
|
+
df[Parameter.SNWD] = df[Parameter.SNWD].multiply(10)
|
|
89
|
+
df[Parameter.VSBY] = df[Parameter.VSBY].multiply(1000)
|
|
90
|
+
|
|
91
|
+
# Change coco
|
|
92
|
+
df[Parameter.COCO] = df[Parameter.COCO].apply(get_coco)
|
|
93
|
+
df[Parameter.CLDC] = df[Parameter.CLDC].apply(percentage_to_okta)
|
|
94
|
+
|
|
95
|
+
# Set index
|
|
96
|
+
df["time"] = pd.to_datetime(
|
|
97
|
+
df["Datum"] + " " + df["Uhrzeit (UTC)"], format="%d.%m.%y %H:%M"
|
|
98
|
+
)
|
|
99
|
+
df = df.set_index(["time"])
|
|
100
|
+
df = df.drop(["Datum", "Uhrzeit (UTC)"], axis=1)
|
|
101
|
+
|
|
102
|
+
return df
|
|
103
|
+
|
|
104
|
+
except HTTPError as error:
|
|
105
|
+
logger.info(
|
|
106
|
+
f"Couldn't load DWD POI data for weather station {station} (status: {error.status})"
|
|
107
|
+
)
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
except Exception as error:
|
|
111
|
+
logger.warning(error)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
|
|
116
|
+
if "wmo" in req.station.identifiers:
|
|
117
|
+
return get_df(req.station.identifiers["wmo"])
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
from ftplib import FTP
|
|
3
|
+
|
|
4
|
+
from meteostat.core.config import config
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
DWD_FTP_SERVER = config.dwd_ftp_host
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_ftp_connection() -> FTP:
|
|
11
|
+
"""
|
|
12
|
+
Get DWD Open Data FTP connection
|
|
13
|
+
"""
|
|
14
|
+
dwd_ftp_connection = FTP(DWD_FTP_SERVER)
|
|
15
|
+
dwd_ftp_connection.login()
|
|
16
|
+
return dwd_ftp_connection
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_condicode(code: str) -> Union[int, None]:
|
|
20
|
+
"""
|
|
21
|
+
Map DWD codes to Meteostat condicodes
|
|
22
|
+
|
|
23
|
+
https://opendata.dwd.de/climate_environment/CDC/observations_germany/climate/hourly/weather_phenomena/recent/Wetter_Beschreibung.txt
|
|
24
|
+
"""
|
|
25
|
+
condicodes = {
|
|
26
|
+
"0": 1,
|
|
27
|
+
"1": 2,
|
|
28
|
+
"2": 1,
|
|
29
|
+
"3": 3,
|
|
30
|
+
"11": 5,
|
|
31
|
+
"12": 5,
|
|
32
|
+
"13": 23,
|
|
33
|
+
"17": 25,
|
|
34
|
+
"21": 8,
|
|
35
|
+
"22": 15,
|
|
36
|
+
"23": 12,
|
|
37
|
+
"24": 10,
|
|
38
|
+
"25": 17,
|
|
39
|
+
"26": 21,
|
|
40
|
+
"27": 24,
|
|
41
|
+
"28": 5,
|
|
42
|
+
"29": 25,
|
|
43
|
+
"40": 5,
|
|
44
|
+
"41": 5,
|
|
45
|
+
"42": 5,
|
|
46
|
+
"43": 5,
|
|
47
|
+
"44": 5,
|
|
48
|
+
"45": 5,
|
|
49
|
+
"46": 5,
|
|
50
|
+
"47": 5,
|
|
51
|
+
"48": 5,
|
|
52
|
+
"49": 5,
|
|
53
|
+
"50": 7,
|
|
54
|
+
"51": 7,
|
|
55
|
+
"52": 8,
|
|
56
|
+
"53": 8,
|
|
57
|
+
"54": 9,
|
|
58
|
+
"55": 9,
|
|
59
|
+
"56": 10,
|
|
60
|
+
"57": 11,
|
|
61
|
+
"58": 7,
|
|
62
|
+
"59": 8,
|
|
63
|
+
"60": 7,
|
|
64
|
+
"61": 7,
|
|
65
|
+
"62": 8,
|
|
66
|
+
"63": 8,
|
|
67
|
+
"64": 9,
|
|
68
|
+
"65": 9,
|
|
69
|
+
"66": 10,
|
|
70
|
+
"67": 11,
|
|
71
|
+
"68": 12,
|
|
72
|
+
"69": 13,
|
|
73
|
+
"70": 14,
|
|
74
|
+
"71": 14,
|
|
75
|
+
"72": 15,
|
|
76
|
+
"73": 15,
|
|
77
|
+
"74": 16,
|
|
78
|
+
"75": 16,
|
|
79
|
+
"80": 17,
|
|
80
|
+
"81": 18,
|
|
81
|
+
"82": 18,
|
|
82
|
+
"83": 19,
|
|
83
|
+
"84": 20,
|
|
84
|
+
"85": 21,
|
|
85
|
+
"86": 22,
|
|
86
|
+
"87": 19,
|
|
87
|
+
"88": 20,
|
|
88
|
+
"89": 24,
|
|
89
|
+
"90": 24,
|
|
90
|
+
"91": 25,
|
|
91
|
+
"92": 26,
|
|
92
|
+
"93": 25,
|
|
93
|
+
"94": 26,
|
|
94
|
+
"95": 25,
|
|
95
|
+
"96": 25,
|
|
96
|
+
"97": 26,
|
|
97
|
+
"98": 25,
|
|
98
|
+
"99": 26,
|
|
99
|
+
"100": 3,
|
|
100
|
+
"101": 2,
|
|
101
|
+
"102": 3,
|
|
102
|
+
"103": 3,
|
|
103
|
+
"120": 5,
|
|
104
|
+
"123": 8,
|
|
105
|
+
"124": 15,
|
|
106
|
+
"125": 10,
|
|
107
|
+
"126": 25,
|
|
108
|
+
"130": 5,
|
|
109
|
+
"131": 5,
|
|
110
|
+
"132": 5,
|
|
111
|
+
"133": 5,
|
|
112
|
+
"134": 5,
|
|
113
|
+
"135": 6,
|
|
114
|
+
"150": 8,
|
|
115
|
+
"151": 7,
|
|
116
|
+
"152": 8,
|
|
117
|
+
"153": 9,
|
|
118
|
+
"154": 10,
|
|
119
|
+
"155": 10,
|
|
120
|
+
"156": 11,
|
|
121
|
+
"157": 7,
|
|
122
|
+
"158": 8,
|
|
123
|
+
"160": 8,
|
|
124
|
+
"161": 7,
|
|
125
|
+
"162": 8,
|
|
126
|
+
"163": 9,
|
|
127
|
+
"164": 10,
|
|
128
|
+
"165": 10,
|
|
129
|
+
"166": 11,
|
|
130
|
+
"167": 12,
|
|
131
|
+
"168": 13,
|
|
132
|
+
"170": 15,
|
|
133
|
+
"171": 14,
|
|
134
|
+
"172": 15,
|
|
135
|
+
"173": 16,
|
|
136
|
+
"177": 14,
|
|
137
|
+
"181": 7,
|
|
138
|
+
"182": 8,
|
|
139
|
+
"183": 9,
|
|
140
|
+
"184": 9,
|
|
141
|
+
"185": 14,
|
|
142
|
+
"186": 15,
|
|
143
|
+
"187": 16,
|
|
144
|
+
"189": 24,
|
|
145
|
+
"190": 25,
|
|
146
|
+
"191": 25,
|
|
147
|
+
"192": 25,
|
|
148
|
+
"193": 25,
|
|
149
|
+
"194": 26,
|
|
150
|
+
"195": 26,
|
|
151
|
+
"196": 26,
|
|
152
|
+
"199": 27,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return condicodes.get(str(code), None)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from meteostat.enumerations import TTL, Parameter
|
|
7
|
+
from meteostat.core.cache import cache_service
|
|
8
|
+
from meteostat.core.network import network_service
|
|
9
|
+
from meteostat.providers.eccc.shared import ENDPOINT, get_meta_data
|
|
10
|
+
from meteostat.typing import ProviderRequest
|
|
11
|
+
|
|
12
|
+
BATCH_LIMIT = 9000
|
|
13
|
+
PROPERTIES = {
|
|
14
|
+
"LOCAL_DATE": "time",
|
|
15
|
+
"MAX_TEMPERATURE": Parameter.TMAX,
|
|
16
|
+
"MEAN_TEMPERATURE": Parameter.TEMP,
|
|
17
|
+
"MIN_TEMPERATURE": Parameter.TMIN,
|
|
18
|
+
"SPEED_MAX_GUST": Parameter.WPGT,
|
|
19
|
+
"TOTAL_PRECIPITATION": Parameter.PRCP,
|
|
20
|
+
"SNOW_ON_GROUND": Parameter.SNWD,
|
|
21
|
+
"TOTAL_SNOW": Parameter.SNOW,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@cache_service.cache(TTL.DAY, "pickle")
|
|
26
|
+
def get_df(climate_id: str, year: int) -> Optional[pd.DataFrame]:
|
|
27
|
+
# Process start & end date
|
|
28
|
+
# ECCC uses the station's local time zone
|
|
29
|
+
start = datetime(year, 1, 1, 0, 0, 0).strftime("%Y-%m-%dT%H:%M:%S")
|
|
30
|
+
end = datetime(year, 12, 31, 23, 59, 59).strftime("%Y-%m-%dT%H:%M:%S")
|
|
31
|
+
|
|
32
|
+
response = network_service.get(
|
|
33
|
+
f"{ENDPOINT}/collections/climate-daily/items",
|
|
34
|
+
params={
|
|
35
|
+
"CLIMATE_IDENTIFIER": climate_id,
|
|
36
|
+
"datetime": f"{start}/{end}",
|
|
37
|
+
"f": "json",
|
|
38
|
+
"properties": ",".join(PROPERTIES.keys()),
|
|
39
|
+
"limit": BATCH_LIMIT,
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
data = response.json()
|
|
44
|
+
|
|
45
|
+
# Extract features from the response
|
|
46
|
+
features = map(
|
|
47
|
+
lambda feature: feature["properties"] if "properties" in feature else {},
|
|
48
|
+
data.get("features", []),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Create a DataFrame from the extracted features
|
|
52
|
+
df = pd.DataFrame(features)
|
|
53
|
+
df = df.rename(columns=PROPERTIES)
|
|
54
|
+
|
|
55
|
+
# Handle time column & set index
|
|
56
|
+
df["time"] = pd.to_datetime(df["time"])
|
|
57
|
+
df = df.set_index(["time"])
|
|
58
|
+
|
|
59
|
+
return df
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def fetch(req: ProviderRequest) -> Optional[pd.DataFrame]:
|
|
63
|
+
if (
|
|
64
|
+
"national" not in req.station.identifiers
|
|
65
|
+
or req.start is None
|
|
66
|
+
or req.end is None
|
|
67
|
+
):
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
meta_data = get_meta_data(req.station.identifiers["national"])
|
|
71
|
+
climate_id = meta_data.get("CLIMATE_IDENTIFIER")
|
|
72
|
+
archive_first = meta_data.get("DLY_FIRST_DATE")
|
|
73
|
+
archive_last = meta_data.get("DLY_LAST_DATE")
|
|
74
|
+
|
|
75
|
+
if not (climate_id and archive_first and archive_last):
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
archive_start = datetime.strptime(archive_first, "%Y-%m-%d %H:%M:%S")
|
|
79
|
+
archive_end = datetime.strptime(archive_last, "%Y-%m-%d %H:%M:%S")
|
|
80
|
+
|
|
81
|
+
years = range(
|
|
82
|
+
max(req.start.year, archive_start.year),
|
|
83
|
+
min(req.end.year, archive_end.year) + 1,
|
|
84
|
+
)
|
|
85
|
+
data = [get_df(climate_id, year) for year in years]
|
|
86
|
+
|
|
87
|
+
return pd.concat(data) if len(data) and not all(d is None for d in data) else None
|