diff --git a/.gitignore b/.gitignore index 4e9d241..375c7f5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ init dist htmlcov __init__.pyc -development/secret.py \ No newline at end of file +development/secret.py +custom_test.py \ No newline at end of file diff --git a/README.md b/README.md index c2bdc29..a3fe727 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ DISCLAIMER: This project is a private open source project and doesn't have any connection with Deutscher Wetterdienst. +## Weather data + This is a python package for simple access to hourly forecast data for the next 10 days. The data is updated every six hours and updated when needed. Available station-IDs can be found [here](simple_dwd_weatherforecast/stations.py) in the third column or you can use the method `dwdforecast.get_nearest_station_id(latitude, longitude)` which tries to find it for you. @@ -22,6 +24,10 @@ Forecasted weather conditions are evaluated using this [table](https://www.dwd.d The weather report for the region which is available on the DWD homepage (see an example [here](https://www.dwd.de/DWD/wetter/wv_allg/deutschland/text/vhdl13_dwoh.html)) can also be retrieved via a method which maps the station to the relevant region. +## Weather maps + +You can also retrieve weather maps from the DWD GeoServer with this package. + ## Installation ```python @@ -30,6 +36,9 @@ python3 -m pip install simple_dwd_weatherforecast ## Usage +### Weather data + +#### Usage example ```python from simple_dwd_weatherforecast import dwdforecast from datetime import datetime, timedelta, timezone @@ -44,7 +53,7 @@ time_tomorrow = datetime.now(timezone.utc)+timedelta(days=1) temperature_tomorrow = dwd_weather.get_forecast_data(dwdforecast.WeatherDataType.TEMPERATURE, time_tomorrow) ``` -### Available methods +#### Available methods All methods return their values as string. The datetime value has to be in UTC. If no data is available for this datetime, None will be returned. With the optional boolean `shouldUpdate` an automated check for new updates can be prevented by setting this parameter to `False`. Otherwise data is updated if new data is available with every function call. @@ -106,10 +115,9 @@ class Weather: get_timeframe_condition(datetime, timeframe: hours after datetime as int, optional bool shouldUpdate) # Result is an approximate "feeled" condition at this time frame get_weather_report(optional bool shouldUpdate) # Returns the weather report for the geographical region of the station as HTML - ``` -### Advanced Usage +#### Advanced Usage If you want to access the forecast data for the next 10 days directly for further processing, you can do so. All data is stored in dictonary and can be accessed like this: @@ -123,6 +131,49 @@ access_forecast_dict = dwd_weather.forecast_data # dwd_weather.forecast_data con Keep in mind that the weather condition is stored as the original digit value as provided by DWD. So if you want to use them, you have to convert these yourself. You can use my simplified conversion from the source code in the variable `weather_codes` or the original conversion available [here](https://www.dwd.de/DE/leistungen/opendata/help/schluessel_datenformate/kml/mosmix_element_weather_xls.xlsx?__blob=publicationFile&v=4). +## Weather maps + +You can download weather maps from the DWD GeoServer with this package. There are different options for the displayed foreground and background data. See below for further information. + +![example picture of a map produced with this package](/map_example.png?raw=true "Example picture of a map produced with this package") + +#### Usage example +```python +from simple_dwd_weatherforecast import dwdmap + +dwdmap.get_from_location(51.272, 8.84, radius_km=100, map_type=dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR, background_type=dwdmap.WeatherBackgroundMapType.BUNDESLAENDER, file_name="map.png") + +dwdmap.get_germany(map_type=dwdmap.WeatherMapType.UVINDEX, width=520, height=580, filename="germany.png") +``` + +#### Available methods + +```python + +class WeatherMapType(Enum): + NIEDERSCHLAGSRADAR = "dwd:Niederschlagsradar" + MAXTEMP = "dwd:GefuehlteTempMax" + UVINDEX = "dwd:UVI_CS" + POLLENFLUG = "dwd:Pollenflug" + SATELLITE_RGB = "dwd:Satellite_meteosat_1km_euat_rgb_day_hrv_and_night_ir108_3h" + SATELLITE_IR = "dwd:Satellite_worldmosaic_3km_world_ir108_3h" + WARNUNGEN_GEMEINDEN = "dwd:Warnungen_Gemeinden" + WARNUNGEN_KREISE = "dwd:Warnungen_Landkreise" + +class WeatherBackgroundMapType(Enum): + LAENDER = "dwd:Laender" + BUNDESLAENDER = "dwd:Warngebiete_Bundeslaender" + KREISE = "dwd:Warngebiete_Kreise" + GEMEINDEN = "dwd:Warngebiete_Gemeinden" + SATELLIT = "dwd:bluemarble" + +def get_from_location(longitude, latitude, radius_km, map_type: WeatherMapType, background_type: WeatherBackgroundMapType, optional integer image_width, optional integer image_height, optional string filename (default:"map.png")) #Saves map with given radius from coordinates + +get_germany(map_type: WeatherMapType, optional integer image_width, optional integer image_height, optional string filename (default:"map.png")) #Saves map of whole germany + +get_map(minx,miny,maxx,maxy, map_type: WeatherMapType, background_type: WeatherBackgroundMapType, optional integer image_width, optional integer image_height, optional string filename (default:"map.png")) #Map retrieval +``` + ## Help and Contribution 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. diff --git a/map_example.png b/map_example.png new file mode 100644 index 0000000..1944899 Binary files /dev/null and b/map_example.png differ diff --git a/setup.py b/setup.py index 7d2edce..15ecd57 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="simple_dwd_weatherforecast", - version="1.1.5", + version="2.0.0", author="Max Fermor", description="A simple tool to retrieve a weather forecast from DWD OpenData", long_description=long_description, diff --git a/simple_dwd_weatherforecast/dwdmap.py b/simple_dwd_weatherforecast/dwdmap.py new file mode 100644 index 0000000..f9dd04d --- /dev/null +++ b/simple_dwd_weatherforecast/dwdmap.py @@ -0,0 +1,46 @@ +import requests +import math +from io import BytesIO +from PIL import Image +from enum import Enum + + +class WeatherMapType(Enum): + NIEDERSCHLAGSRADAR = "dwd:Niederschlagsradar" + MAXTEMP = "dwd:GefuehlteTempMax" + UVINDEX = "dwd:UVI_CS" + POLLENFLUG = "dwd:Pollenflug" + SATELLITE_RGB = "dwd:Satellite_meteosat_1km_euat_rgb_day_hrv_and_night_ir108_3h" + SATELLITE_IR = "dwd:Satellite_worldmosaic_3km_world_ir108_3h" + WARNUNGEN_GEMEINDEN = "dwd:Warnungen_Gemeinden" + WARNUNGEN_KREISE = "dwd:Warnungen_Landkreise" + +class WeatherBackgroundMapType(Enum): + LAENDER = "dwd:Laender" + BUNDESLAENDER = "dwd:Warngebiete_Bundeslaender" + KREISE = "dwd:Warngebiete_Kreise" + GEMEINDEN = "dwd:Warngebiete_Gemeinden" + SATELLIT = "dwd:bluemarble" + +def get_from_location(longitude, latitude, radius_km, map_type: WeatherMapType, background_type: WeatherBackgroundMapType = WeatherBackgroundMapType.BUNDESLAENDER, image_width=520, image_height=580, filename="map.png"): + if radius_km <= 0: + raise ValueError("Radius must be greater than 0") + if latitude < -90 or latitude > 90: + raise ValueError("Latitude must be between -90 and 90") + if longitude < -180 or longitude > 180: + raise ValueError("Longitude must be between -180 and 180") + radius = math.fabs(radius_km / (111.3 * math.cos(latitude))) + get_map(latitude-radius, longitude-radius, latitude+radius, longitude+radius, map_type, background_type, image_width, image_height, filename) + +def get_germany(map_type: WeatherMapType, image_width=520, image_height=580, filename="map.png"): + get_map(4.4, 46.4, 16.1, 55.6, map_type, WeatherBackgroundMapType.BUNDESLAENDER, image_width, image_height, filename) + +def get_map(minx,miny,maxx,maxy, map_type: WeatherMapType, background_type: WeatherBackgroundMapType, image_width=520, image_height=580, filename="map.png"): + if image_width > 1200 or image_height > 1400: + raise ValueError("Width and height must not exceed 1200 and 1400 respectively. Please be kind to the DWD servers.") + + 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" + request = requests.get(url, stream=True) + if request.status_code == 200: + image = Image.open(BytesIO(request.content)) + image.save(filename) \ No newline at end of file diff --git a/tests/test_map.py b/tests/test_map.py new file mode 100644 index 0000000..8165c18 --- /dev/null +++ b/tests/test_map.py @@ -0,0 +1,16 @@ +import unittest +from simple_dwd_weatherforecast import dwdmap + + +class MapTestCase(unittest.TestCase): + def test_prevent_too_large_height(self): + with self.assertRaises(ValueError): + dwdmap.get_map(4.4, 46.4, 16.1, 55.6, dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR, dwdmap.WeatherBackgroundMapType.BUNDESLAENDER, 1300, 700) + + def test_prevent_too_large_width(self): + with self.assertRaises(ValueError): + dwdmap.get_map(4.4, 46.4, 16.1, 55.6, dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR, dwdmap.WeatherBackgroundMapType.BUNDESLAENDER, 300, 1700) + + def test_prevent_too_large(self): + with self.assertRaises(ValueError): + dwdmap.get_map(4.4, 46.4, 16.1, 55.6, dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR, dwdmap.WeatherBackgroundMapType.BUNDESLAENDER, 1300, 1700)