simple-dwd-weatherforecast 2.0.29__tar.gz → 2.0.32__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.29/simple_dwd_weatherforecast.egg-info → simple_dwd_weatherforecast-2.0.32}/PKG-INFO +3 -1
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/README.md +2 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/setup.py +1 -1
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/simple_dwd_weatherforecast/dwdforecast.py +194 -158
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/simple_dwd_weatherforecast/dwdmap.py +47 -8
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32/simple_dwd_weatherforecast.egg-info}/PKG-INFO +3 -1
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/simple_dwd_weatherforecast.egg-info/SOURCES.txt +1 -0
- simple_dwd_weatherforecast-2.0.32/tests/test_get_daily_avg.py +48 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_get_daily_sum.py +2 -2
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_get_day_values.py +20 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/LICENCE +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/setup.cfg +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/simple_dwd_weatherforecast/__init__.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/simple_dwd_weatherforecast/stations.json +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/simple_dwd_weatherforecast/uv_stations.json +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/simple_dwd_weatherforecast.egg-info/dependency_links.txt +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/simple_dwd_weatherforecast.egg-info/requires.txt +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/simple_dwd_weatherforecast.egg-info/top_level.txt +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/__init__.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/dummy_data.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/dummy_data_full.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/dummy_uv.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_get_daily_condition.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_get_daily_max.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_get_daily_min.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_get_forecast_condition.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_get_forecast_data.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_get_station_name.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_get_timeframe_avg.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_get_timeframe_condition.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_get_timeframe_max.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_get_timeframe_min.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_get_timeframe_sum.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_get_timeframe_values.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_is_in_timerange.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_is_valid_timeframe.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_location_tools.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_map.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_parsekml.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_region.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_reported_weather.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_station.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_stationsfile.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_update.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_update_hourly.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_uv_index.py +0 -0
- {simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/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.0.32
|
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
|
@@ -127,6 +127,8 @@ class Weather:
|
|
127
127
|
|
128
128
|
get_timeframe_sum(weatherDataType: see WeatherDataType, datetime, timeframe: hours after datetime as int, optional bool shouldUpdate) # Returns the sum of that value within the time frame
|
129
129
|
|
130
|
+
get_daily_avg(weatherDataType: see WeatherDataType, datetime, optional bool shouldUpdate) # Returns the daily average of that value
|
131
|
+
|
130
132
|
get_timeframe_avg(weatherDataType: see WeatherDataType, datetime, timeframe: hours after datetime as int, optional bool shouldUpdate) # Returns the average of that value within the time frame
|
131
133
|
|
132
134
|
get_forecast_condition(datetime, optional bool shouldUpdate) # Result is condition as text
|
@@ -110,6 +110,8 @@ class Weather:
|
|
110
110
|
|
111
111
|
get_timeframe_sum(weatherDataType: see WeatherDataType, datetime, timeframe: hours after datetime as int, optional bool shouldUpdate) # Returns the sum of that value within the time frame
|
112
112
|
|
113
|
+
get_daily_avg(weatherDataType: see WeatherDataType, datetime, optional bool shouldUpdate) # Returns the daily average of that value
|
114
|
+
|
113
115
|
get_timeframe_avg(weatherDataType: see WeatherDataType, datetime, timeframe: hours after datetime as int, optional bool shouldUpdate) # Returns the average of that value within the time frame
|
114
116
|
|
115
117
|
get_forecast_condition(datetime, optional bool shouldUpdate) # Result is condition as text
|
@@ -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.0.32",
|
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,
|
@@ -400,8 +400,8 @@ class Weather:
|
|
400
400
|
else:
|
401
401
|
print("no report for this station available. Have you updated first?")
|
402
402
|
|
403
|
-
def get_uv_index(self, days_from_today: int) -> int:
|
404
|
-
if not self.uv_reports:
|
403
|
+
def get_uv_index(self, days_from_today: int, shouldUpdate=True) -> int:
|
404
|
+
if not self.uv_reports and shouldUpdate:
|
405
405
|
self.update(
|
406
406
|
force_hourly=False,
|
407
407
|
with_forecast=False,
|
@@ -418,7 +418,11 @@ class Weather:
|
|
418
418
|
day = "tomorrow"
|
419
419
|
elif days_from_today == 2:
|
420
420
|
day = "dayafter_to"
|
421
|
-
return
|
421
|
+
return (
|
422
|
+
self.uv_reports[self.nearest_uv_index_station]["forecast"][day]
|
423
|
+
if self.uv_reports
|
424
|
+
else None
|
425
|
+
)
|
422
426
|
|
423
427
|
def get_timeframe_max(
|
424
428
|
self,
|
@@ -542,6 +546,15 @@ class Weather:
|
|
542
546
|
)
|
543
547
|
return None
|
544
548
|
|
549
|
+
def get_daily_avg(
|
550
|
+
self, weatherDataType: WeatherDataType, timestamp: datetime, shouldUpdate=True
|
551
|
+
):
|
552
|
+
if shouldUpdate:
|
553
|
+
self.update()
|
554
|
+
if self.is_in_timerange(timestamp):
|
555
|
+
return self.get_avg(self.get_day_values(timestamp), weatherDataType)
|
556
|
+
return None
|
557
|
+
|
545
558
|
def get_avg(_, weather_data, weatherDataType):
|
546
559
|
value_sum = 0.0
|
547
560
|
count = len(weather_data)
|
@@ -582,20 +595,16 @@ class Weather:
|
|
582
595
|
time_step += timedelta(hours=1)
|
583
596
|
else:
|
584
597
|
time_step = first_entry_date
|
585
|
-
endtime = (
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
)
|
596
|
-
+ timedelta(days=1)
|
597
|
-
+ timedelta(hours=-1)
|
598
|
-
)
|
598
|
+
endtime = datetime(
|
599
|
+
time_step.year,
|
600
|
+
time_step.month,
|
601
|
+
time_step.day,
|
602
|
+
0,
|
603
|
+
0,
|
604
|
+
0,
|
605
|
+
0,
|
606
|
+
timezone.utc,
|
607
|
+
) + timedelta(days=1)
|
599
608
|
timediff = endtime - time_step
|
600
609
|
for _ in range(round(timediff.total_seconds() / 3600)):
|
601
610
|
result.append(self.forecast_data[self.strip_to_hour_str(time_step)])
|
@@ -766,79 +775,92 @@ class Weather:
|
|
766
775
|
self.report_data = {
|
767
776
|
"time": row["Parameter description"],
|
768
777
|
"date": row["surface observations"],
|
769
|
-
WeatherDataType.CONDITION.value[0]:
|
770
|
-
row[WeatherDataType.CONDITION.value[1]]
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
WeatherDataType.TEMPERATURE.value[0]:
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
778
|
+
WeatherDataType.CONDITION.value[0]: (
|
779
|
+
int(row[WeatherDataType.CONDITION.value[1]])
|
780
|
+
if row[WeatherDataType.CONDITION.value[1]] != self.NOT_AVAILABLE
|
781
|
+
else None
|
782
|
+
),
|
783
|
+
WeatherDataType.TEMPERATURE.value[0]: (
|
784
|
+
round(
|
785
|
+
float(
|
786
|
+
row[WeatherDataType.TEMPERATURE.value[1]]
|
787
|
+
.replace(self.NOT_AVAILABLE, "0.0")
|
788
|
+
.replace(",", ".")
|
789
|
+
)
|
790
|
+
+ 273.1,
|
791
|
+
1,
|
779
792
|
)
|
780
|
-
|
781
|
-
|
782
|
-
)
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
WeatherDataType.WIND_SPEED.value[0]:
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
)
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
WeatherDataType.
|
831
|
-
float(row[WeatherDataType.
|
832
|
-
|
833
|
-
|
834
|
-
)
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
793
|
+
if row[WeatherDataType.TEMPERATURE.value[1]] != self.NOT_AVAILABLE
|
794
|
+
else None
|
795
|
+
),
|
796
|
+
WeatherDataType.DEWPOINT.value[0]: (
|
797
|
+
round(
|
798
|
+
float(row[WeatherDataType.DEWPOINT.value[1]].replace(",", "."))
|
799
|
+
+ 273.1,
|
800
|
+
1,
|
801
|
+
)
|
802
|
+
if row[WeatherDataType.DEWPOINT.value[1]] != self.NOT_AVAILABLE
|
803
|
+
else None
|
804
|
+
),
|
805
|
+
WeatherDataType.PRESSURE.value[0]: (
|
806
|
+
float(row[WeatherDataType.PRESSURE.value[1]].replace(",", ".")) * 100
|
807
|
+
if row[WeatherDataType.PRESSURE.value[1]] != self.NOT_AVAILABLE
|
808
|
+
else None
|
809
|
+
),
|
810
|
+
WeatherDataType.WIND_SPEED.value[0]: (
|
811
|
+
round(
|
812
|
+
float(row[WeatherDataType.WIND_SPEED.value[1]].replace(",", "."))
|
813
|
+
/ 3.6,
|
814
|
+
1,
|
815
|
+
)
|
816
|
+
if row[WeatherDataType.WIND_SPEED.value[1]] != self.NOT_AVAILABLE
|
817
|
+
else None
|
818
|
+
),
|
819
|
+
WeatherDataType.WIND_DIRECTION.value[0]: (
|
820
|
+
int(row[WeatherDataType.WIND_DIRECTION.value[1]])
|
821
|
+
if row[WeatherDataType.WIND_DIRECTION.value[1]] != self.NOT_AVAILABLE
|
822
|
+
else None
|
823
|
+
),
|
824
|
+
WeatherDataType.WIND_GUSTS.value[0]: (
|
825
|
+
round(
|
826
|
+
float(row[WeatherDataType.WIND_GUSTS.value[1]].replace(",", "."))
|
827
|
+
/ 3.6,
|
828
|
+
1,
|
829
|
+
)
|
830
|
+
if row[WeatherDataType.WIND_GUSTS.value[1]] != self.NOT_AVAILABLE
|
831
|
+
else None
|
832
|
+
),
|
833
|
+
WeatherDataType.PRECIPITATION.value[0]: (
|
834
|
+
float(row[WeatherDataType.PRECIPITATION.value[1]].replace(",", "."))
|
835
|
+
if row[WeatherDataType.PRECIPITATION.value[1]] != self.NOT_AVAILABLE
|
836
|
+
else None
|
837
|
+
),
|
838
|
+
WeatherDataType.CLOUD_COVERAGE.value[0]: (
|
839
|
+
float(row[WeatherDataType.CLOUD_COVERAGE.value[1]].replace(",", "."))
|
840
|
+
if row[WeatherDataType.CLOUD_COVERAGE.value[1]] != self.NOT_AVAILABLE
|
841
|
+
else None
|
842
|
+
),
|
843
|
+
WeatherDataType.VISIBILITY.value[0]: (
|
844
|
+
float(row[WeatherDataType.VISIBILITY.value[1]].replace(",", ".")) * 1e3
|
845
|
+
if row[WeatherDataType.VISIBILITY.value[1]] != self.NOT_AVAILABLE
|
846
|
+
else None
|
847
|
+
),
|
848
|
+
WeatherDataType.SUN_IRRADIANCE.value[0]: (
|
849
|
+
round(
|
850
|
+
float(
|
851
|
+
row[WeatherDataType.SUN_IRRADIANCE.value[1]].replace(",", ".")
|
852
|
+
)
|
853
|
+
* 3.6,
|
854
|
+
1,
|
855
|
+
)
|
856
|
+
if row[WeatherDataType.SUN_IRRADIANCE.value[1]] != self.NOT_AVAILABLE
|
857
|
+
else None
|
858
|
+
),
|
859
|
+
WeatherDataType.HUMIDITY.value[0]: (
|
860
|
+
float(row[WeatherDataType.HUMIDITY.value[1]].replace(",", "."))
|
861
|
+
if row[WeatherDataType.HUMIDITY.value[1]] != self.NOT_AVAILABLE
|
862
|
+
else None
|
863
|
+
),
|
842
864
|
}
|
843
865
|
|
844
866
|
def get_weather_report(self, shouldUpdate=False):
|
@@ -852,21 +874,24 @@ class Weather:
|
|
852
874
|
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.116 Safari/537.36"
|
853
875
|
}
|
854
876
|
headers["If-None-Match"] = self.etags[url] if url in self.etags else ""
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
877
|
+
try:
|
878
|
+
request = requests.get(url, headers=headers, timeout=30)
|
879
|
+
# If resource has not been modified, return
|
880
|
+
if request.status_code == 304:
|
881
|
+
return
|
882
|
+
elif request.status_code != 200:
|
883
|
+
raise Exception(f"Unexpected status code {request.status_code}")
|
884
|
+
self.etags[url] = request.headers["ETag"]
|
885
|
+
uv_reports = json.loads(request.text)["content"]
|
886
|
+
# Match with existing stations
|
887
|
+
for uv_report in uv_reports:
|
888
|
+
station = get_station_by_name(
|
889
|
+
self.uv_index_stations_reference_names[uv_report["city"]]
|
890
|
+
)
|
891
|
+
# uv_report.update({"lat": station[1]["lat"], "lon": station[1]["lon"]})
|
892
|
+
self.uv_reports[station[0]] = uv_report
|
893
|
+
except Exception as error:
|
894
|
+
print(f"Error in download_weather_report: {type(error)} args: {error.args}")
|
870
895
|
|
871
896
|
def download_weather_report(self, region_code):
|
872
897
|
url = f"https://www.dwd.de/DWD/wetter/wv_allg/deutschland/text/vhdl13_{region_code}.html"
|
@@ -874,16 +899,19 @@ class Weather:
|
|
874
899
|
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.116 Safari/537.36"
|
875
900
|
}
|
876
901
|
headers["If-None-Match"] = self.etags[url] if url in self.etags else ""
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
902
|
+
try:
|
903
|
+
request = requests.get(url, headers=headers, timeout=30)
|
904
|
+
# If resource has not been modified, return
|
905
|
+
if request.status_code == 304:
|
906
|
+
return
|
907
|
+
self.etags[url] = request.headers["ETag"]
|
908
|
+
weather_report = request.text
|
909
|
+
a = weather_report.find(">")
|
910
|
+
if a != -1:
|
911
|
+
weather_report = weather_report[a + 1 :]
|
912
|
+
self.weather_report = weather_report
|
913
|
+
except Exception as error:
|
914
|
+
print(f"Error in download_weather_report: {type(error)} args: {error.args}")
|
887
915
|
|
888
916
|
def download_latest_kml(self, stationid, force_hourly=False):
|
889
917
|
if force_hourly:
|
@@ -891,15 +919,18 @@ class Weather:
|
|
891
919
|
else:
|
892
920
|
url = f"https://opendata.dwd.de/weather/local_forecasts/mos/MOSMIX_L/single_stations/{stationid}/kml/MOSMIX_L_LATEST_{stationid}.kmz"
|
893
921
|
headers = {"If-None-Match": self.etags[url] if url in self.etags else ""}
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
922
|
+
try:
|
923
|
+
request = requests.get(url, headers=headers, timeout=30)
|
924
|
+
# If resource has not been modified, return
|
925
|
+
if request.status_code == 304:
|
926
|
+
return
|
927
|
+
self.etags[url] = request.headers["ETag"]
|
928
|
+
with ZipFile(BytesIO(request.content), "r") as kmz:
|
929
|
+
# large RAM allocation
|
930
|
+
with kmz.open(kmz.namelist()[0], "r") as kml:
|
931
|
+
self.parse_kml(kml, force_hourly)
|
932
|
+
except Exception as error:
|
933
|
+
print(f"Error in download_weather_report: {type(error)} args: {error.args}")
|
903
934
|
|
904
935
|
def download_latest_report(self):
|
905
936
|
station_id = self.station_id
|
@@ -909,38 +940,43 @@ class Weather:
|
|
909
940
|
f"https://opendata.dwd.de/weather/weather_reports/poi/{station_id}-BEOB.csv"
|
910
941
|
)
|
911
942
|
headers = {"If-None-Match": self.etags[url] if url in self.etags else ""}
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
row["
|
933
|
-
"
|
934
|
-
]
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
943
|
+
try:
|
944
|
+
response = requests.get(url, headers=headers, timeout=30)
|
945
|
+
if response.status_code == 200:
|
946
|
+
content = response.content
|
947
|
+
reader = csv.DictReader(
|
948
|
+
content.decode("utf-8").splitlines(), delimiter=";"
|
949
|
+
)
|
950
|
+
is_first_run = True
|
951
|
+
backuprows = []
|
952
|
+
for row in reader:
|
953
|
+
if is_first_run:
|
954
|
+
is_first_run = False
|
955
|
+
reader.__next__()
|
956
|
+
continue
|
957
|
+
backuprows.append(reader.__next__())
|
958
|
+
backuprows.append(reader.__next__())
|
959
|
+
backuprows.append(reader.__next__())
|
960
|
+
|
961
|
+
# Some items are only reported each hour
|
962
|
+
for backuprow in backuprows:
|
963
|
+
if row["cloud_cover_total"] == self.NOT_AVAILABLE:
|
964
|
+
row["cloud_cover_total"] = backuprow["cloud_cover_total"]
|
965
|
+
if row["horizontal_visibility"] == self.NOT_AVAILABLE:
|
966
|
+
row["horizontal_visibility"] = backuprow[
|
967
|
+
"horizontal_visibility"
|
968
|
+
]
|
969
|
+
if row["present_weather"] == self.NOT_AVAILABLE:
|
970
|
+
row["present_weather"] = backuprow["present_weather"]
|
971
|
+
self.parse_csv_row(row)
|
972
|
+
# We only want the latest report
|
973
|
+
break
|
974
|
+
|
975
|
+
elif response.status_code == 304:
|
976
|
+
# The report is already up to date
|
977
|
+
print("Report is already up to date")
|
978
|
+
else:
|
979
|
+
# Handle other status codes
|
980
|
+
print(f"Failed to download report. Status code: {response.status_code}")
|
981
|
+
except Exception as error:
|
982
|
+
print(f"Error in download_weather_report: {type(error)} args: {error.args}")
|
@@ -15,6 +15,7 @@ class WeatherMapType(Enum):
|
|
15
15
|
WARNUNGEN_GEMEINDEN = "dwd:Warnungen_Gemeinden"
|
16
16
|
WARNUNGEN_KREISE = "dwd:Warnungen_Landkreise"
|
17
17
|
|
18
|
+
|
18
19
|
class WeatherBackgroundMapType(Enum):
|
19
20
|
LAENDER = "dwd:Laender"
|
20
21
|
BUNDESLAENDER = "dwd:Warngebiete_Bundeslaender"
|
@@ -23,7 +24,16 @@ class WeatherBackgroundMapType(Enum):
|
|
23
24
|
SATELLIT = "dwd:bluemarble"
|
24
25
|
GEWAESSER = "dwd:Gewaesser"
|
25
26
|
|
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
|
+
):
|
27
37
|
if radius_km <= 0:
|
28
38
|
raise ValueError("Radius must be greater than 0")
|
29
39
|
if latitude < -90 or latitude > 90:
|
@@ -31,17 +41,46 @@ def get_from_location(longitude, latitude, radius_km, map_type: WeatherMapType,
|
|
31
41
|
if longitude < -180 or longitude > 180:
|
32
42
|
raise ValueError("Longitude must be between -180 and 180")
|
33
43
|
radius = math.fabs(radius_km / (111.3 * math.cos(latitude)))
|
34
|
-
return get_map(
|
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
|
+
)
|
35
65
|
|
36
|
-
def get_germany(map_type: WeatherMapType, background_type: WeatherBackgroundMapType = WeatherBackgroundMapType.BUNDESLAENDER, image_width=520, image_height=580):
|
37
|
-
return get_map(4.4, 46.4, 16.1, 55.6, map_type, background_type, image_width, image_height)
|
38
66
|
|
39
|
-
def get_map(
|
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
|
+
):
|
40
77
|
if image_width > 1200 or image_height > 1400:
|
41
|
-
raise ValueError(
|
78
|
+
raise ValueError(
|
79
|
+
"Width and height must not exceed 1200 and 1400 respectively. Please be kind to the DWD servers."
|
80
|
+
)
|
42
81
|
|
43
|
-
url = f"https://maps.dwd.de/geoserver/dwd/wms?service=WMS&version=1.1.0&request=GetMap&layers={
|
82
|
+
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"
|
44
83
|
request = requests.get(url, stream=True)
|
45
84
|
if request.status_code == 200:
|
46
85
|
image = Image.open(BytesIO(request.content))
|
47
|
-
return image
|
86
|
+
return image
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: simple_dwd_weatherforecast
|
3
|
-
Version: 2.0.
|
3
|
+
Version: 2.0.32
|
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
|
@@ -127,6 +127,8 @@ class Weather:
|
|
127
127
|
|
128
128
|
get_timeframe_sum(weatherDataType: see WeatherDataType, datetime, timeframe: hours after datetime as int, optional bool shouldUpdate) # Returns the sum of that value within the time frame
|
129
129
|
|
130
|
+
get_daily_avg(weatherDataType: see WeatherDataType, datetime, optional bool shouldUpdate) # Returns the daily average of that value
|
131
|
+
|
130
132
|
get_timeframe_avg(weatherDataType: see WeatherDataType, datetime, timeframe: hours after datetime as int, optional bool shouldUpdate) # Returns the average of that value within the time frame
|
131
133
|
|
132
134
|
get_forecast_condition(datetime, optional bool shouldUpdate) # Result is condition as text
|
@@ -0,0 +1,48 @@
|
|
1
|
+
from simple_dwd_weatherforecast.dwdforecast import WeatherDataType
|
2
|
+
import unittest
|
3
|
+
from unittest.mock import patch
|
4
|
+
from datetime import datetime
|
5
|
+
from simple_dwd_weatherforecast import dwdforecast
|
6
|
+
from dummy_data import parsed_data
|
7
|
+
|
8
|
+
|
9
|
+
class Weather_get_daily_avg(unittest.TestCase):
|
10
|
+
def setUp(self):
|
11
|
+
self.dwd_weather = dwdforecast.Weather("H889")
|
12
|
+
self.dwd_weather.forecast_data = parsed_data
|
13
|
+
self.dwd_weather.station_name = "BAD HOMBURG"
|
14
|
+
|
15
|
+
@patch("simple_dwd_weatherforecast.dwdforecast.Weather.update", return_value=None)
|
16
|
+
def test_shouldupdate(self, mock_update):
|
17
|
+
test_time = datetime(2020, 11, 7, 3, 30)
|
18
|
+
self.dwd_weather.get_daily_avg(WeatherDataType.PRECIPITATION, test_time, True)
|
19
|
+
mock_update.assert_called()
|
20
|
+
|
21
|
+
@patch("simple_dwd_weatherforecast.dwdforecast.Weather.update", return_value=None)
|
22
|
+
def test_shouldupdate_not(self, mock_update):
|
23
|
+
test_time = datetime(2020, 11, 7, 3, 30)
|
24
|
+
self.dwd_weather.get_daily_avg(WeatherDataType.PRECIPITATION, test_time, False)
|
25
|
+
mock_update.assert_not_called()
|
26
|
+
|
27
|
+
def test_not_in_timerange(self):
|
28
|
+
test_time = datetime(2000, 11, 7, 3, 30)
|
29
|
+
self.assertIsNone(
|
30
|
+
self.dwd_weather.get_daily_avg(WeatherDataType.PRECIPITATION, test_time)
|
31
|
+
)
|
32
|
+
|
33
|
+
@patch("simple_dwd_weatherforecast.dwdforecast.Weather.update", return_value=None)
|
34
|
+
def test_precipitation(self, mock_update):
|
35
|
+
test_time = datetime(2020, 11, 6, 12, 0)
|
36
|
+
print(self.dwd_weather.get_day_values(test_time))
|
37
|
+
self.assertEqual(
|
38
|
+
self.dwd_weather.get_daily_avg(WeatherDataType.PRECIPITATION, test_time),
|
39
|
+
1.8,
|
40
|
+
)
|
41
|
+
|
42
|
+
@patch("simple_dwd_weatherforecast.dwdforecast.Weather.update", return_value=None)
|
43
|
+
def test_temperature(self, mock_update):
|
44
|
+
test_time = datetime(2020, 11, 6, 23, 0)
|
45
|
+
self.assertEqual(
|
46
|
+
self.dwd_weather.get_daily_avg(WeatherDataType.TEMPERATURE, test_time),
|
47
|
+
278.09,
|
48
|
+
)
|
{simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_get_daily_sum.py
RENAMED
@@ -35,7 +35,7 @@ class Weather_get_daily_sum(unittest.TestCase):
|
|
35
35
|
test_time = datetime(2020, 11, 6, 10, 0)
|
36
36
|
self.assertEqual(
|
37
37
|
self.dwd_weather.get_daily_sum(WeatherDataType.PRECIPITATION, test_time),
|
38
|
-
|
38
|
+
36.01,
|
39
39
|
)
|
40
40
|
|
41
41
|
@patch("simple_dwd_weatherforecast.dwdforecast.Weather.update", return_value=None)
|
@@ -43,5 +43,5 @@ class Weather_get_daily_sum(unittest.TestCase):
|
|
43
43
|
test_time = datetime(2020, 11, 6, 10, 0)
|
44
44
|
self.assertEqual(
|
45
45
|
self.dwd_weather.get_daily_sum(WeatherDataType.TEMPERATURE, test_time),
|
46
|
-
|
46
|
+
5561.8,
|
47
47
|
)
|
{simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_get_day_values.py
RENAMED
@@ -451,6 +451,7 @@ class Weather_get_day_values(unittest.TestCase):
|
|
451
451
|
self.dwd_weather.get_day_values(test_time),
|
452
452
|
test_data,
|
453
453
|
)
|
454
|
+
self.assertEqual(len(self.dwd_weather.get_day_values(test_time)), 24)
|
454
455
|
|
455
456
|
def test_day_not_last_day(self):
|
456
457
|
test_time = datetime(2020, 11, 16, 1, 0)
|
@@ -1004,8 +1005,27 @@ class Weather_get_day_values(unittest.TestCase):
|
|
1004
1005
|
"wwM": 3.0,
|
1005
1006
|
"humidity": 81.3,
|
1006
1007
|
},
|
1008
|
+
{
|
1009
|
+
"TTT": 275.55,
|
1010
|
+
"Td": 273.45,
|
1011
|
+
"condition": "51",
|
1012
|
+
"PPPP": 103060.0,
|
1013
|
+
"DD": 52.0,
|
1014
|
+
"FF": 1.54,
|
1015
|
+
"FX1": 3.09,
|
1016
|
+
"RR1c": 8.76,
|
1017
|
+
"wwP": 2.0,
|
1018
|
+
"DRR1": 0.0,
|
1019
|
+
"N": 22.0,
|
1020
|
+
"VV": 15500.0,
|
1021
|
+
"SunD1": 0.0,
|
1022
|
+
"Rad1h": None,
|
1023
|
+
"wwM": 2.0,
|
1024
|
+
"humidity": 86.0,
|
1025
|
+
},
|
1007
1026
|
]
|
1008
1027
|
self.assertEqual(
|
1009
1028
|
self.dwd_weather.get_day_values(test_time),
|
1010
1029
|
test_data,
|
1011
1030
|
)
|
1031
|
+
self.assertEqual(len(self.dwd_weather.get_day_values(test_time)), 20)
|
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.29 → simple_dwd_weatherforecast-2.0.32}/tests/dummy_data_full.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_get_daily_max.py
RENAMED
File without changes
|
{simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_get_daily_min.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
|
File without changes
|
File without changes
|
{simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_location_tools.py
RENAMED
File without changes
|
File without changes
|
{simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_parsekml.py
RENAMED
File without changes
|
{simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_region.py
RENAMED
File without changes
|
File without changes
|
{simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_station.py
RENAMED
File without changes
|
{simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_stationsfile.py
RENAMED
File without changes
|
{simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_update.py
RENAMED
File without changes
|
{simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_update_hourly.py
RENAMED
File without changes
|
{simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_uv_index.py
RENAMED
File without changes
|
{simple_dwd_weatherforecast-2.0.29 → simple_dwd_weatherforecast-2.0.32}/tests/test_weather.py
RENAMED
File without changes
|