simple-dwd-weatherforecast 2.0.34__tar.gz → 2.1.0__tar.gz
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.
- {simple_dwd_weatherforecast-2.0.34/simple_dwd_weatherforecast.egg-info → simple_dwd_weatherforecast-2.1.0}/PKG-INFO +39 -2
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/README.md +38 -1
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/setup.py +1 -1
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/simple_dwd_weatherforecast/dwdforecast.py +0 -2
- simple_dwd_weatherforecast-2.1.0/simple_dwd_weatherforecast/dwdmap.py +182 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0/simple_dwd_weatherforecast.egg-info}/PKG-INFO +39 -2
- simple_dwd_weatherforecast-2.1.0/tests/test_map.py +171 -0
- simple_dwd_weatherforecast-2.0.34/simple_dwd_weatherforecast/dwdmap.py +0 -85
- simple_dwd_weatherforecast-2.0.34/tests/test_map.py +0 -16
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/LICENCE +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/setup.cfg +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/simple_dwd_weatherforecast/__init__.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/simple_dwd_weatherforecast/stations.json +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/simple_dwd_weatherforecast/uv_stations.json +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/simple_dwd_weatherforecast.egg-info/SOURCES.txt +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/simple_dwd_weatherforecast.egg-info/dependency_links.txt +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/simple_dwd_weatherforecast.egg-info/requires.txt +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/simple_dwd_weatherforecast.egg-info/top_level.txt +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/__init__.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/dummy_data.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/dummy_data_full.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/dummy_uv.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_daily_avg.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_daily_condition.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_daily_max.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_daily_min.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_daily_sum.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_day_values.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_forecast_condition.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_forecast_data.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_station_name.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_timeframe_avg.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_timeframe_condition.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_timeframe_max.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_timeframe_min.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_timeframe_sum.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_timeframe_values.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_is_in_timerange.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_is_valid_timeframe.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_location_tools.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_parsekml.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_region.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_reported_weather.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_station.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_stationsfile.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_update.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_update_hourly.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_uv_index.py +0 -0
- {simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_weather.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: simple_dwd_weatherforecast
|
3
|
-
Version: 2.0
|
3
|
+
Version: 2.1.0
|
4
4
|
Summary: A simple tool to retrieve a weather forecast from DWD OpenData
|
5
5
|
Home-page: https://github.com/FL550/simple_dwd_weatherforecast.git
|
6
6
|
Author: Max Fermor
|
@@ -198,13 +198,50 @@ class WeatherBackgroundMapType(Enum):
|
|
198
198
|
GEMEINDEN = "dwd:Warngebiete_Gemeinden"
|
199
199
|
SATELLIT = "dwd:bluemarble"
|
200
200
|
|
201
|
-
|
201
|
+
get_from_location(longitude, latitude, radius_km, map_type: WeatherMapType, background_type: WeatherBackgroundMapType, optional integer image_width, optional integer image_height) #Returns map as pillow image with given radius from coordinates
|
202
202
|
|
203
203
|
get_germany(map_type: WeatherMapType, optional WeatherBackgroundMapType background_type, optional integer image_width, optional integer image_height, optional string save_to_filename) #Returns map as pillow image of whole germany
|
204
204
|
|
205
205
|
get_map(minx,miny,maxx,maxy, map_type: WeatherMapType, background_type: WeatherBackgroundMapType, optional integer image_width, optional integer image_height, optional string save_to_filename) #Returns map as pillow image
|
206
206
|
```
|
207
207
|
|
208
|
+
|
209
|
+
### Image loop
|
210
|
+
|
211
|
+
There is also the possibility to retrieve multiple images as a loop. This can be done by the class ImageLoop.
|
212
|
+
|
213
|
+
|
214
|
+
#### Usage example
|
215
|
+
```python
|
216
|
+
from simple_dwd_weatherforecast import dwdmap
|
217
|
+
|
218
|
+
maploop = dwdmap.ImageLoop(
|
219
|
+
dwdmap.germany_boundaries.minx,
|
220
|
+
dwdmap.germany_boundaries.miny,
|
221
|
+
dwdmap.germany_boundaries.maxx,
|
222
|
+
dwdmap.germany_boundaries.maxy,
|
223
|
+
dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
|
224
|
+
dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
|
225
|
+
steps=5,
|
226
|
+
)
|
227
|
+
|
228
|
+
for image in enumerate(maploop._images):
|
229
|
+
image[1].save(f"image{image[0]}.png")
|
230
|
+
|
231
|
+
```
|
232
|
+
|
233
|
+
#### Available methods
|
234
|
+
|
235
|
+
```python
|
236
|
+
ImageLoop(minx: float, miny: float, maxx: float, maxy: float, map_type: WeatherMapType, background_type: WeatherBackgroundMapType,
|
237
|
+
steps: int = 6, image_width: int = 520,image_height: int = 580) -> ImageLoop
|
238
|
+
|
239
|
+
get_images() -> Iterable[ImageFile.ImageFile] # Returns the image loop
|
240
|
+
|
241
|
+
update() # Updates the loop to the most recent files
|
242
|
+
|
243
|
+
```
|
244
|
+
|
208
245
|
## Help and Contribution
|
209
246
|
|
210
247
|
Feel free to open an issue if you find one and I will do my best to help you. If you want to contribute, your help is appreciated! If you want to add a new feature, add a pull request first so we can chat about the details.
|
@@ -181,13 +181,50 @@ class WeatherBackgroundMapType(Enum):
|
|
181
181
|
GEMEINDEN = "dwd:Warngebiete_Gemeinden"
|
182
182
|
SATELLIT = "dwd:bluemarble"
|
183
183
|
|
184
|
-
|
184
|
+
get_from_location(longitude, latitude, radius_km, map_type: WeatherMapType, background_type: WeatherBackgroundMapType, optional integer image_width, optional integer image_height) #Returns map as pillow image with given radius from coordinates
|
185
185
|
|
186
186
|
get_germany(map_type: WeatherMapType, optional WeatherBackgroundMapType background_type, optional integer image_width, optional integer image_height, optional string save_to_filename) #Returns map as pillow image of whole germany
|
187
187
|
|
188
188
|
get_map(minx,miny,maxx,maxy, map_type: WeatherMapType, background_type: WeatherBackgroundMapType, optional integer image_width, optional integer image_height, optional string save_to_filename) #Returns map as pillow image
|
189
189
|
```
|
190
190
|
|
191
|
+
|
192
|
+
### Image loop
|
193
|
+
|
194
|
+
There is also the possibility to retrieve multiple images as a loop. This can be done by the class ImageLoop.
|
195
|
+
|
196
|
+
|
197
|
+
#### Usage example
|
198
|
+
```python
|
199
|
+
from simple_dwd_weatherforecast import dwdmap
|
200
|
+
|
201
|
+
maploop = dwdmap.ImageLoop(
|
202
|
+
dwdmap.germany_boundaries.minx,
|
203
|
+
dwdmap.germany_boundaries.miny,
|
204
|
+
dwdmap.germany_boundaries.maxx,
|
205
|
+
dwdmap.germany_boundaries.maxy,
|
206
|
+
dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
|
207
|
+
dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
|
208
|
+
steps=5,
|
209
|
+
)
|
210
|
+
|
211
|
+
for image in enumerate(maploop._images):
|
212
|
+
image[1].save(f"image{image[0]}.png")
|
213
|
+
|
214
|
+
```
|
215
|
+
|
216
|
+
#### Available methods
|
217
|
+
|
218
|
+
```python
|
219
|
+
ImageLoop(minx: float, miny: float, maxx: float, maxy: float, map_type: WeatherMapType, background_type: WeatherBackgroundMapType,
|
220
|
+
steps: int = 6, image_width: int = 520,image_height: int = 580) -> ImageLoop
|
221
|
+
|
222
|
+
get_images() -> Iterable[ImageFile.ImageFile] # Returns the image loop
|
223
|
+
|
224
|
+
update() # Updates the loop to the most recent files
|
225
|
+
|
226
|
+
```
|
227
|
+
|
191
228
|
## Help and Contribution
|
192
229
|
|
193
230
|
Feel free to open an issue if you find one and I will do my best to help you. If you want to contribute, your help is appreciated! If you want to add a new feature, add a pull request first so we can chat about the details.
|
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
|
|
5
5
|
|
6
6
|
setuptools.setup(
|
7
7
|
name="simple_dwd_weatherforecast",
|
8
|
-
version="2.0
|
8
|
+
version="2.1.0",
|
9
9
|
author="Max Fermor",
|
10
10
|
description="A simple tool to retrieve a weather forecast from DWD OpenData",
|
11
11
|
long_description=long_description,
|
@@ -358,11 +358,9 @@ class Weather:
|
|
358
358
|
return None
|
359
359
|
|
360
360
|
def get_daily_condition(self, timestamp: datetime, shouldUpdate=True):
|
361
|
-
print("testieng")
|
362
361
|
if shouldUpdate:
|
363
362
|
self.update()
|
364
363
|
if self.is_in_timerange_day(timestamp):
|
365
|
-
print("working")
|
366
364
|
return self.get_condition(self.get_day_values(timestamp))
|
367
365
|
return None
|
368
366
|
|
@@ -0,0 +1,182 @@
|
|
1
|
+
from typing import Iterable
|
2
|
+
import requests
|
3
|
+
import math
|
4
|
+
from io import BytesIO
|
5
|
+
from PIL import Image, ImageFile
|
6
|
+
from enum import Enum
|
7
|
+
from collections import deque
|
8
|
+
from datetime import datetime, timedelta, timezone
|
9
|
+
|
10
|
+
|
11
|
+
class WeatherMapType(Enum):
|
12
|
+
NIEDERSCHLAGSRADAR = "dwd:Niederschlagsradar,dwd:NCEW_EU"
|
13
|
+
MAXTEMP = "dwd:GefuehlteTempMax"
|
14
|
+
UVINDEX = "dwd:UVI_CS"
|
15
|
+
POLLENFLUG = "dwd:Pollenflug"
|
16
|
+
SATELLITE_RGB = "dwd:Satellite_meteosat_1km_euat_rgb_day_hrv_and_night_ir108_3h"
|
17
|
+
SATELLITE_IR = "dwd:Satellite_worldmosaic_3km_world_ir108_3h"
|
18
|
+
WARNUNGEN_GEMEINDEN = "dwd:Warnungen_Gemeinden"
|
19
|
+
WARNUNGEN_KREISE = "dwd:Warnungen_Landkreise"
|
20
|
+
|
21
|
+
|
22
|
+
class WeatherBackgroundMapType(Enum):
|
23
|
+
LAENDER = "dwd:Laender"
|
24
|
+
BUNDESLAENDER = "dwd:Warngebiete_Bundeslaender"
|
25
|
+
KREISE = "dwd:Warngebiete_Kreise"
|
26
|
+
GEMEINDEN = "dwd:Warngebiete_Gemeinden"
|
27
|
+
SATELLIT = "dwd:bluemarble"
|
28
|
+
GEWAESSER = "dwd:Gewaesser"
|
29
|
+
|
30
|
+
|
31
|
+
class germany_boundaries:
|
32
|
+
minx = 4.4
|
33
|
+
miny = 46.4
|
34
|
+
maxx = 16.1
|
35
|
+
maxy = 55.6
|
36
|
+
|
37
|
+
|
38
|
+
def get_from_location(
|
39
|
+
longitude,
|
40
|
+
latitude,
|
41
|
+
radius_km,
|
42
|
+
map_type: WeatherMapType,
|
43
|
+
background_type: WeatherBackgroundMapType = WeatherBackgroundMapType.BUNDESLAENDER,
|
44
|
+
image_width=520,
|
45
|
+
image_height=580,
|
46
|
+
):
|
47
|
+
if radius_km <= 0:
|
48
|
+
raise ValueError("Radius must be greater than 0")
|
49
|
+
if latitude < -90 or latitude > 90:
|
50
|
+
raise ValueError("Latitude must be between -90 and 90")
|
51
|
+
if longitude < -180 or longitude > 180:
|
52
|
+
raise ValueError("Longitude must be between -180 and 180")
|
53
|
+
radius = math.fabs(radius_km / (111.3 * math.cos(latitude)))
|
54
|
+
return get_map(
|
55
|
+
latitude - radius,
|
56
|
+
longitude - radius,
|
57
|
+
latitude + radius,
|
58
|
+
longitude + radius,
|
59
|
+
map_type,
|
60
|
+
background_type,
|
61
|
+
image_width,
|
62
|
+
image_height,
|
63
|
+
)
|
64
|
+
|
65
|
+
|
66
|
+
def get_germany(
|
67
|
+
map_type: WeatherMapType,
|
68
|
+
background_type: WeatherBackgroundMapType = WeatherBackgroundMapType.BUNDESLAENDER,
|
69
|
+
image_width=520,
|
70
|
+
image_height=580,
|
71
|
+
):
|
72
|
+
return get_map(
|
73
|
+
germany_boundaries.minx,
|
74
|
+
germany_boundaries.miny,
|
75
|
+
germany_boundaries.maxx,
|
76
|
+
germany_boundaries.maxy,
|
77
|
+
map_type,
|
78
|
+
background_type,
|
79
|
+
image_width,
|
80
|
+
image_height,
|
81
|
+
)
|
82
|
+
|
83
|
+
|
84
|
+
def get_map(
|
85
|
+
minx,
|
86
|
+
miny,
|
87
|
+
maxx,
|
88
|
+
maxy,
|
89
|
+
map_type: WeatherMapType,
|
90
|
+
background_type: WeatherBackgroundMapType,
|
91
|
+
image_width=520,
|
92
|
+
image_height=580,
|
93
|
+
):
|
94
|
+
if image_width > 1200 or image_height > 1400:
|
95
|
+
raise ValueError(
|
96
|
+
"Width and height must not exceed 1200 and 1400 respectively. Please be kind to the DWD servers."
|
97
|
+
)
|
98
|
+
url = f"https://maps.dwd.de/geoserver/dwd/wms?service=WMS&version=1.1.0&request=GetMap&layers={map_type.value},{background_type.value}&bbox={minx},{miny},{maxx},{maxy}&width={image_width}&height={image_height}&srs=EPSG:4326&styles=&format=image/png"
|
99
|
+
request = requests.get(url, stream=True)
|
100
|
+
if request.status_code == 200:
|
101
|
+
image = Image.open(BytesIO(request.content))
|
102
|
+
return image
|
103
|
+
|
104
|
+
|
105
|
+
class ImageLoop:
|
106
|
+
_last_update: datetime
|
107
|
+
_images: deque[ImageFile.ImageFile]
|
108
|
+
|
109
|
+
_minx: float
|
110
|
+
_miny: float
|
111
|
+
_maxx: float
|
112
|
+
_maxy: float
|
113
|
+
_map_type: WeatherMapType
|
114
|
+
_background_type: WeatherBackgroundMapType
|
115
|
+
_image_width: int
|
116
|
+
_image_height: int
|
117
|
+
|
118
|
+
def __init__(
|
119
|
+
self,
|
120
|
+
minx: float,
|
121
|
+
miny: float,
|
122
|
+
maxx: float,
|
123
|
+
maxy: float,
|
124
|
+
map_type: WeatherMapType,
|
125
|
+
background_type: WeatherBackgroundMapType,
|
126
|
+
steps: int = 6,
|
127
|
+
image_width: int = 520,
|
128
|
+
image_height: int = 580,
|
129
|
+
):
|
130
|
+
if image_width > 1200 or image_height > 1400:
|
131
|
+
raise ValueError(
|
132
|
+
"Width and height must not exceed 1200 and 1400 respectively. Please be kind to the DWD servers."
|
133
|
+
)
|
134
|
+
self._minx = minx
|
135
|
+
self._miny = miny
|
136
|
+
self._maxx = maxx
|
137
|
+
self._maxy = maxy
|
138
|
+
self._map_type = map_type
|
139
|
+
self._background_type = background_type
|
140
|
+
self._steps = steps
|
141
|
+
self._image_width = image_width
|
142
|
+
self._image_height = image_height
|
143
|
+
|
144
|
+
self._images = deque([], steps)
|
145
|
+
|
146
|
+
self._full_reload()
|
147
|
+
|
148
|
+
def get_images(self) -> Iterable[ImageFile.ImageFile]:
|
149
|
+
return self._images
|
150
|
+
|
151
|
+
def _full_reload(self):
|
152
|
+
self._images.clear()
|
153
|
+
now = get_time_last_5_min(datetime.now(timezone.utc))
|
154
|
+
self._last_update = now - timedelta(minutes=5) * self._steps
|
155
|
+
|
156
|
+
while now > self._last_update:
|
157
|
+
self._last_update += timedelta(minutes=5)
|
158
|
+
self._images.append(self._get_image(self._last_update))
|
159
|
+
|
160
|
+
def update(self):
|
161
|
+
now = get_time_last_5_min(datetime.now(timezone.utc))
|
162
|
+
|
163
|
+
# If last update is older than the buffer can hold, reload the whole buffer
|
164
|
+
if now - self._last_update > self._steps * timedelta(minutes=5):
|
165
|
+
self._full_reload()
|
166
|
+
# Update the buffer and fetch only the new images
|
167
|
+
else:
|
168
|
+
while now > self._last_update:
|
169
|
+
self._last_update += timedelta(minutes=5)
|
170
|
+
self._images.append(self._get_image(self._last_update))
|
171
|
+
|
172
|
+
def _get_image(self, date: datetime) -> ImageFile.ImageFile:
|
173
|
+
url = f"https://maps.dwd.de/geoserver/dwd/wms?service=WMS&version=1.1.0&request=GetMap&layers={self._map_type.value},{self._background_type.value}&bbox={self._minx},{self._miny},{self._maxx},{self._maxy}&width={self._image_width}&height={self._image_height}&srs=EPSG:4326&styles=&format=image/png&TIME={date.strftime("%Y-%m-%dT%H:%M:00.0Z")}"
|
174
|
+
request = requests.get(url, stream=True)
|
175
|
+
if request.status_code != 200:
|
176
|
+
raise ConnectionError("Error during image request from DWD servers")
|
177
|
+
return Image.open(BytesIO(request.content))
|
178
|
+
|
179
|
+
|
180
|
+
def get_time_last_5_min(date: datetime) -> datetime:
|
181
|
+
minute = math.floor(date.minute / 5) * 5
|
182
|
+
return date.replace(minute=minute, second=0, microsecond=0)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: simple_dwd_weatherforecast
|
3
|
-
Version: 2.0
|
3
|
+
Version: 2.1.0
|
4
4
|
Summary: A simple tool to retrieve a weather forecast from DWD OpenData
|
5
5
|
Home-page: https://github.com/FL550/simple_dwd_weatherforecast.git
|
6
6
|
Author: Max Fermor
|
@@ -198,13 +198,50 @@ class WeatherBackgroundMapType(Enum):
|
|
198
198
|
GEMEINDEN = "dwd:Warngebiete_Gemeinden"
|
199
199
|
SATELLIT = "dwd:bluemarble"
|
200
200
|
|
201
|
-
|
201
|
+
get_from_location(longitude, latitude, radius_km, map_type: WeatherMapType, background_type: WeatherBackgroundMapType, optional integer image_width, optional integer image_height) #Returns map as pillow image with given radius from coordinates
|
202
202
|
|
203
203
|
get_germany(map_type: WeatherMapType, optional WeatherBackgroundMapType background_type, optional integer image_width, optional integer image_height, optional string save_to_filename) #Returns map as pillow image of whole germany
|
204
204
|
|
205
205
|
get_map(minx,miny,maxx,maxy, map_type: WeatherMapType, background_type: WeatherBackgroundMapType, optional integer image_width, optional integer image_height, optional string save_to_filename) #Returns map as pillow image
|
206
206
|
```
|
207
207
|
|
208
|
+
|
209
|
+
### Image loop
|
210
|
+
|
211
|
+
There is also the possibility to retrieve multiple images as a loop. This can be done by the class ImageLoop.
|
212
|
+
|
213
|
+
|
214
|
+
#### Usage example
|
215
|
+
```python
|
216
|
+
from simple_dwd_weatherforecast import dwdmap
|
217
|
+
|
218
|
+
maploop = dwdmap.ImageLoop(
|
219
|
+
dwdmap.germany_boundaries.minx,
|
220
|
+
dwdmap.germany_boundaries.miny,
|
221
|
+
dwdmap.germany_boundaries.maxx,
|
222
|
+
dwdmap.germany_boundaries.maxy,
|
223
|
+
dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
|
224
|
+
dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
|
225
|
+
steps=5,
|
226
|
+
)
|
227
|
+
|
228
|
+
for image in enumerate(maploop._images):
|
229
|
+
image[1].save(f"image{image[0]}.png")
|
230
|
+
|
231
|
+
```
|
232
|
+
|
233
|
+
#### Available methods
|
234
|
+
|
235
|
+
```python
|
236
|
+
ImageLoop(minx: float, miny: float, maxx: float, maxy: float, map_type: WeatherMapType, background_type: WeatherBackgroundMapType,
|
237
|
+
steps: int = 6, image_width: int = 520,image_height: int = 580) -> ImageLoop
|
238
|
+
|
239
|
+
get_images() -> Iterable[ImageFile.ImageFile] # Returns the image loop
|
240
|
+
|
241
|
+
update() # Updates the loop to the most recent files
|
242
|
+
|
243
|
+
```
|
244
|
+
|
208
245
|
## Help and Contribution
|
209
246
|
|
210
247
|
Feel free to open an issue if you find one and I will do my best to help you. If you want to contribute, your help is appreciated! If you want to add a new feature, add a pull request first so we can chat about the details.
|
@@ -0,0 +1,171 @@
|
|
1
|
+
import unittest
|
2
|
+
from unittest.mock import patch
|
3
|
+
from simple_dwd_weatherforecast import dwdmap
|
4
|
+
from datetime import datetime, timedelta, timezone
|
5
|
+
|
6
|
+
|
7
|
+
class MapTestCase(unittest.TestCase):
|
8
|
+
def test_prevent_too_large_height(self):
|
9
|
+
with self.assertRaises(ValueError):
|
10
|
+
dwdmap.get_map(
|
11
|
+
4.4,
|
12
|
+
46.4,
|
13
|
+
16.1,
|
14
|
+
55.6,
|
15
|
+
dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
|
16
|
+
dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
|
17
|
+
1300,
|
18
|
+
700,
|
19
|
+
)
|
20
|
+
|
21
|
+
def test_prevent_too_large_width(self):
|
22
|
+
with self.assertRaises(ValueError):
|
23
|
+
dwdmap.get_map(
|
24
|
+
4.4,
|
25
|
+
46.4,
|
26
|
+
16.1,
|
27
|
+
55.6,
|
28
|
+
dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
|
29
|
+
dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
|
30
|
+
300,
|
31
|
+
1700,
|
32
|
+
)
|
33
|
+
|
34
|
+
def test_prevent_too_large(self):
|
35
|
+
with self.assertRaises(ValueError):
|
36
|
+
dwdmap.get_map(
|
37
|
+
4.4,
|
38
|
+
46.4,
|
39
|
+
16.1,
|
40
|
+
55.6,
|
41
|
+
dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
|
42
|
+
dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
|
43
|
+
1300,
|
44
|
+
1700,
|
45
|
+
)
|
46
|
+
|
47
|
+
|
48
|
+
class ImageLoopTestCase(unittest.TestCase):
|
49
|
+
@patch("simple_dwd_weatherforecast.dwdmap.ImageLoop._get_image", return_value=None)
|
50
|
+
def test_init(self, mock_function):
|
51
|
+
maploop = dwdmap.ImageLoop(
|
52
|
+
0.1,
|
53
|
+
0.2,
|
54
|
+
0.3,
|
55
|
+
0.4,
|
56
|
+
dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
|
57
|
+
dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
|
58
|
+
1,
|
59
|
+
)
|
60
|
+
self.assertEqual(
|
61
|
+
maploop._minx,
|
62
|
+
0.1,
|
63
|
+
)
|
64
|
+
self.assertEqual(
|
65
|
+
maploop._miny,
|
66
|
+
0.2,
|
67
|
+
)
|
68
|
+
self.assertEqual(
|
69
|
+
maploop._maxx,
|
70
|
+
0.3,
|
71
|
+
)
|
72
|
+
self.assertEqual(
|
73
|
+
maploop._maxy,
|
74
|
+
0.4,
|
75
|
+
)
|
76
|
+
self.assertEqual(
|
77
|
+
maploop._map_type,
|
78
|
+
dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
|
79
|
+
)
|
80
|
+
self.assertEqual(
|
81
|
+
maploop._background_type,
|
82
|
+
dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
|
83
|
+
)
|
84
|
+
mock_function.assert_called_once()
|
85
|
+
|
86
|
+
@patch("simple_dwd_weatherforecast.dwdmap.ImageLoop._get_image", return_value=None)
|
87
|
+
def test_steps1(self, mock_function):
|
88
|
+
_ = dwdmap.ImageLoop(
|
89
|
+
0.1,
|
90
|
+
0.2,
|
91
|
+
0.3,
|
92
|
+
0.4,
|
93
|
+
dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
|
94
|
+
dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
|
95
|
+
5,
|
96
|
+
)
|
97
|
+
self.assertEqual(mock_function.call_count, 5)
|
98
|
+
|
99
|
+
@patch("simple_dwd_weatherforecast.dwdmap.ImageLoop._get_image", return_value=None)
|
100
|
+
def test_steps2(self, mock_function):
|
101
|
+
_ = dwdmap.ImageLoop(
|
102
|
+
0.1,
|
103
|
+
0.2,
|
104
|
+
0.3,
|
105
|
+
0.4,
|
106
|
+
dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
|
107
|
+
dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
|
108
|
+
10,
|
109
|
+
)
|
110
|
+
self.assertEqual(mock_function.call_count, 10)
|
111
|
+
|
112
|
+
@patch("simple_dwd_weatherforecast.dwdmap.ImageLoop._get_image", return_value=None)
|
113
|
+
def test_update_full(self, mock_function):
|
114
|
+
maploop = dwdmap.ImageLoop(
|
115
|
+
0.1,
|
116
|
+
0.2,
|
117
|
+
0.3,
|
118
|
+
0.4,
|
119
|
+
dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
|
120
|
+
dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
|
121
|
+
5,
|
122
|
+
)
|
123
|
+
maploop._last_update = datetime(year=2024, month=9, day=4, hour=5, minute=50)
|
124
|
+
self.assertEqual(mock_function.call_count, 5)
|
125
|
+
|
126
|
+
@patch("simple_dwd_weatherforecast.dwdmap.ImageLoop._get_image", return_value=None)
|
127
|
+
def test_update_part(self, mock_function):
|
128
|
+
maploop = dwdmap.ImageLoop(
|
129
|
+
0.1,
|
130
|
+
0.2,
|
131
|
+
0.3,
|
132
|
+
0.4,
|
133
|
+
dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
|
134
|
+
dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
|
135
|
+
5,
|
136
|
+
)
|
137
|
+
now = dwdmap.get_time_last_5_min(datetime.now(timezone.utc))
|
138
|
+
maploop._last_update = now - timedelta(minutes=15)
|
139
|
+
mock_function.reset_mock()
|
140
|
+
maploop.update()
|
141
|
+
self.assertEqual(mock_function.call_count, 3)
|
142
|
+
|
143
|
+
|
144
|
+
class GeneralTestCase(unittest.TestCase):
|
145
|
+
def test_get_time_last_5_min_strip(self):
|
146
|
+
time = datetime(year=2024, month=9, day=4, hour=9, minute=50, second=52)
|
147
|
+
self.assertEqual(
|
148
|
+
dwdmap.get_time_last_5_min(time),
|
149
|
+
datetime(year=2024, month=9, day=4, hour=9, minute=50),
|
150
|
+
)
|
151
|
+
|
152
|
+
def test_get_time_last_5_min_exact(self):
|
153
|
+
time = datetime(year=2024, month=9, day=4, hour=9, minute=50, second=00)
|
154
|
+
self.assertEqual(
|
155
|
+
dwdmap.get_time_last_5_min(time),
|
156
|
+
datetime(year=2024, month=9, day=4, hour=9, minute=50),
|
157
|
+
)
|
158
|
+
|
159
|
+
def test_get_time_last_5_min_between1(self):
|
160
|
+
time = datetime(year=2024, month=9, day=4, hour=9, minute=52, second=00)
|
161
|
+
self.assertEqual(
|
162
|
+
dwdmap.get_time_last_5_min(time),
|
163
|
+
datetime(year=2024, month=9, day=4, hour=9, minute=50),
|
164
|
+
)
|
165
|
+
|
166
|
+
def test_get_time_last_5_min_between2(self):
|
167
|
+
time = datetime(year=2024, month=9, day=4, hour=9, minute=3, second=00)
|
168
|
+
self.assertEqual(
|
169
|
+
dwdmap.get_time_last_5_min(time),
|
170
|
+
datetime(year=2024, month=9, day=4, hour=9, minute=0),
|
171
|
+
)
|
@@ -1,85 +0,0 @@
|
|
1
|
-
import requests
|
2
|
-
import math
|
3
|
-
from io import BytesIO
|
4
|
-
from PIL import Image
|
5
|
-
from enum import Enum
|
6
|
-
|
7
|
-
|
8
|
-
class WeatherMapType(Enum):
|
9
|
-
NIEDERSCHLAGSRADAR = "dwd:Niederschlagsradar,dwd:NCEW_EU"
|
10
|
-
MAXTEMP = "dwd:GefuehlteTempMax"
|
11
|
-
UVINDEX = "dwd:UVI_CS"
|
12
|
-
POLLENFLUG = "dwd:Pollenflug"
|
13
|
-
SATELLITE_RGB = "dwd:Satellite_meteosat_1km_euat_rgb_day_hrv_and_night_ir108_3h"
|
14
|
-
SATELLITE_IR = "dwd:Satellite_worldmosaic_3km_world_ir108_3h"
|
15
|
-
WARNUNGEN_GEMEINDEN = "dwd:Warnungen_Gemeinden"
|
16
|
-
WARNUNGEN_KREISE = "dwd:Warnungen_Landkreise"
|
17
|
-
|
18
|
-
|
19
|
-
class WeatherBackgroundMapType(Enum):
|
20
|
-
LAENDER = "dwd:Laender"
|
21
|
-
BUNDESLAENDER = "dwd:Warngebiete_Bundeslaender"
|
22
|
-
KREISE = "dwd:Warngebiete_Kreise"
|
23
|
-
GEMEINDEN = "dwd:Warngebiete_Gemeinden"
|
24
|
-
SATELLIT = "dwd:bluemarble"
|
25
|
-
GEWAESSER = "dwd:Gewaesser"
|
26
|
-
|
27
|
-
|
28
|
-
def get_from_location(
|
29
|
-
longitude,
|
30
|
-
latitude,
|
31
|
-
radius_km,
|
32
|
-
map_type: WeatherMapType,
|
33
|
-
background_type: WeatherBackgroundMapType = WeatherBackgroundMapType.BUNDESLAENDER,
|
34
|
-
image_width=520,
|
35
|
-
image_height=580,
|
36
|
-
):
|
37
|
-
if radius_km <= 0:
|
38
|
-
raise ValueError("Radius must be greater than 0")
|
39
|
-
if latitude < -90 or latitude > 90:
|
40
|
-
raise ValueError("Latitude must be between -90 and 90")
|
41
|
-
if longitude < -180 or longitude > 180:
|
42
|
-
raise ValueError("Longitude must be between -180 and 180")
|
43
|
-
radius = math.fabs(radius_km / (111.3 * math.cos(latitude)))
|
44
|
-
return get_map(
|
45
|
-
latitude - radius,
|
46
|
-
longitude - radius,
|
47
|
-
latitude + radius,
|
48
|
-
longitude + radius,
|
49
|
-
map_type,
|
50
|
-
background_type,
|
51
|
-
image_width,
|
52
|
-
image_height,
|
53
|
-
)
|
54
|
-
|
55
|
-
|
56
|
-
def get_germany(
|
57
|
-
map_type: WeatherMapType,
|
58
|
-
background_type: WeatherBackgroundMapType = WeatherBackgroundMapType.BUNDESLAENDER,
|
59
|
-
image_width=520,
|
60
|
-
image_height=580,
|
61
|
-
):
|
62
|
-
return get_map(
|
63
|
-
4.4, 46.4, 16.1, 55.6, map_type, background_type, image_width, image_height
|
64
|
-
)
|
65
|
-
|
66
|
-
|
67
|
-
def get_map(
|
68
|
-
minx,
|
69
|
-
miny,
|
70
|
-
maxx,
|
71
|
-
maxy,
|
72
|
-
map_type: WeatherMapType,
|
73
|
-
background_type: WeatherBackgroundMapType,
|
74
|
-
image_width=520,
|
75
|
-
image_height=580,
|
76
|
-
):
|
77
|
-
if image_width > 1200 or image_height > 1400:
|
78
|
-
raise ValueError(
|
79
|
-
"Width and height must not exceed 1200 and 1400 respectively. Please be kind to the DWD servers."
|
80
|
-
)
|
81
|
-
url = f"https://maps.dwd.de/geoserver/dwd/wms?service=WMS&version=1.1.0&request=GetMap&layers={map_type.value},{background_type.value}&bbox={minx},{miny},{maxx},{maxy}&width={image_width}&height={image_height}&srs=EPSG:4326&styles=&format=image/png"
|
82
|
-
request = requests.get(url, stream=True)
|
83
|
-
if request.status_code == 200:
|
84
|
-
image = Image.open(BytesIO(request.content))
|
85
|
-
return image
|
@@ -1,16 +0,0 @@
|
|
1
|
-
import unittest
|
2
|
-
from simple_dwd_weatherforecast import dwdmap
|
3
|
-
|
4
|
-
|
5
|
-
class MapTestCase(unittest.TestCase):
|
6
|
-
def test_prevent_too_large_height(self):
|
7
|
-
with self.assertRaises(ValueError):
|
8
|
-
dwdmap.get_map(4.4, 46.4, 16.1, 55.6, dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR, dwdmap.WeatherBackgroundMapType.BUNDESLAENDER, 1300, 700)
|
9
|
-
|
10
|
-
def test_prevent_too_large_width(self):
|
11
|
-
with self.assertRaises(ValueError):
|
12
|
-
dwdmap.get_map(4.4, 46.4, 16.1, 55.6, dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR, dwdmap.WeatherBackgroundMapType.BUNDESLAENDER, 300, 1700)
|
13
|
-
|
14
|
-
def test_prevent_too_large(self):
|
15
|
-
with self.assertRaises(ValueError):
|
16
|
-
dwdmap.get_map(4.4, 46.4, 16.1, 55.6, dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR, dwdmap.WeatherBackgroundMapType.BUNDESLAENDER, 1300, 1700)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/dummy_data_full.py
RENAMED
File without changes
|
File without changes
|
{simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_daily_avg.py
RENAMED
File without changes
|
File without changes
|
{simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_daily_max.py
RENAMED
File without changes
|
{simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_daily_min.py
RENAMED
File without changes
|
{simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_daily_sum.py
RENAMED
File without changes
|
{simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_day_values.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_is_in_timerange.py
RENAMED
File without changes
|
File without changes
|
{simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_location_tools.py
RENAMED
File without changes
|
{simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_parsekml.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_station.py
RENAMED
File without changes
|
{simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_stationsfile.py
RENAMED
File without changes
|
File without changes
|
{simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_update_hourly.py
RENAMED
File without changes
|
{simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_uv_index.py
RENAMED
File without changes
|
{simple_dwd_weatherforecast-2.0.34 → simple_dwd_weatherforecast-2.1.0}/tests/test_weather.py
RENAMED
File without changes
|