Skip to content

Commit

Permalink
Adds UV index
Browse files Browse the repository at this point in the history
  • Loading branch information
FL550 committed Jan 1, 2024
1 parent b37cca4 commit 043cfb4
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 2 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ class Weather:

get_weather_report(optional bool shouldUpdate) # Returns the weather report for the geographical region of the station as HTML

get_uv_index(int day_from_today (values: 0-2)) # Returns the UV index for the nearest station available for today, tomorrow or the day after tomorrow

update(self, optional bool force_hourly (default: False), optional bool with_forecast (default: True), optional bool with_measurements (default: False), optional bool with_report (default: False)) # Updates the weather data
```

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="simple_dwd_weatherforecast",
version="2.0.24",
version="2.0.25",
author="Max Fermor",
description="A simple tool to retrieve a weather forecast from DWD OpenData",
long_description=long_description,
Expand Down
101 changes: 101 additions & 0 deletions simple_dwd_weatherforecast/dwdforecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ def load_station_id(station_id: str):
return None


def get_station_by_name(station_name: str):
for station in stations.items():
if station[1]["name"] == station_name:
return station


def get_nearest_station_id(lat: float, lon: float):
return get_stations_sorted_by_distance(lat, lon)[0][0]

Expand Down Expand Up @@ -101,6 +107,7 @@ class Weather:
forecast_data = None
report_data = None
weather_report = None
uv_reports = {}
etags = None

namespaces = {
Expand Down Expand Up @@ -193,15 +200,77 @@ class Weather:
"MV": "dwph", # Mecklenburg-Vorpommern
}

uv_index_stations = {
"Weinbiet": "Weinbiet",
"Hamburg": "Hamburg Innenstadt",
"Seehausen": "Seehausen",
"Osnabrück": "Osnabrueck",
"Leipzig": "Leipzig",
"Stuttgart": "Stuttgart-Schn.",
"Frankfurt/Main": "Frankfurt/M",
"Norderney": "Norderney",
"Berlin": "Berlin-Alex.",
"Großer Arber": "Gr.Arber",
"Weimar": "Weimar",
"Sankt Peter-Ording": "St.Peter-Ording",
"Konstanz": "Konstanz",
"Düsseldorf": "Duesseldorf",
"Freiburg": "Freiburg",
"Magdeburg": "Magdeburg",
"Wernigerode": "Wernigerode",
"Neubrandenburg": "Neubrandenburg",
"Bonn": "Bonn-Roleber",
"Marienleuchte": "Bisdorf",
"Cottbus": "Cottbus",
"Kiel": "Kiel-H.",
"List auf Sylt": "List/Sylt",
"Arkona": "Arkona",
"Hannover": "Hannover",
"München": "Muenchen Stadt",
"Waren": "Waren",
"Kahler Asten": "K.Asten",
"Hahn": "Hahn",
"Bremen": "Bremen",
"Würzburg": "Wuerzburg",
"Rostock": "Rostock-Stadt",
"Ulm": "Ulm",
"Regensburg": "Regensburg",
"Kassel": "Kassel",
"Dresden": "Dresden-Stadt",
"Zugspitze": "Zugspitze",
"Nürnberg": "Nuernberg",
}

def __init__(self, station_id):
self.etags = {}
self.station = load_station_id(station_id)
if self.station:
self.station_id = station_id
self.region = get_region(station_id)
self.download_uv_index()
self.nearest_uv_index_station = self.get_nearest_station_id_with_uv()
else:
raise ValueError("Not a valid station_id")

def get_nearest_station_id_with_uv(self):
nearest_distance = float("inf")
nearest_station_id = None
for station in self.uv_reports.items():
distance = get_distance(
self.station["lat"],
self.station["lon"],
station[1]["lat"],
station[1]["lon"],
)

if distance < nearest_distance:
nearest_distance = distance
nearest_station_id = get_station_by_name(
self.uv_index_stations[station[1]["city"]]
)

return nearest_station_id

def get_station_name(self):
return self.station["name"]

Expand Down Expand Up @@ -329,6 +398,18 @@ def get_reported_weather(self, weatherDataType: WeatherDataType, shouldUpdate=Tr
else:
print("no report for this station available. Have you updated first?")

def get_uv_index(self, days_from_today: int) -> int:
if days_from_today < 0 or days_from_today > 2:
print("days_from_today must be between 0 and 2")
return None
if days_from_today == 0:
day = "today"
elif days_from_today == 1:
day = "tomorrow"
elif days_from_today == 2:
day = "dayafter_to"
return self.uv_reports[self.nearest_uv_index_station[0]]["forecast"][day]

def get_timeframe_max(
self,
weatherDataType: WeatherDataType,
Expand Down Expand Up @@ -752,6 +833,26 @@ def get_weather_report(self, shouldUpdate=False):
self.update(with_report=True)
return self.weather_report

def download_uv_index(self):
url = "https://opendata.dwd.de/climate_environment/health/alerts/uvi.json"
headers = {
"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"
}
headers["If-None-Match"] = self.etags[url] if url in self.etags else ""
request = requests.get(url, headers=headers)
# If resource has not been modified, return
if request.status_code == 304:
return
elif request.status_code != 200:
raise Exception(f"Unexpected status code {request.status_code}")
self.etags[url] = request.headers["ETag"]
uv_reports = json.loads(request.text)["content"]
# Match with existing stations
for uv_report in uv_reports:
station = get_station_by_name(self.uv_index_stations[uv_report["city"]])
uv_report.update({"lat": station[1]["lat"], "lon": station[1]["lon"]})
self.uv_reports[station[0]] = uv_report

def download_weather_report(self, region_code):
url = f"https://www.dwd.de/DWD/wetter/wv_allg/deutschland/text/vhdl13_{region_code}.html"
headers = {
Expand Down
1 change: 1 addition & 0 deletions tests/dummy_uv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
parsed_data = {'10724': {'city': 'Weinbiet', 'forecast': {'dayafter_to': 1, 'tomorrow': 0, 'today': 1}, 'lat': 49.383, 'lon': 8.117}, 'P0489': {'city': 'Hamburg', 'forecast': {'dayafter_to': 0, 'tomorrow': 0, 'today': 0}, 'lat': 53.55, 'lon': 9.983}, '10261': {'forecast': {'tomorrow': 0, 'dayafter_to': 0, 'today': 0}, 'city': 'Seehausen', 'lat': 52.883, 'lon': 11.733}, '10317': {'city': 'Osnabrück', 'forecast': {'dayafter_to': 0, 'tomorrow': 0, 'today': 0}, 'lat': 52.25, 'lon': 8.05}, '10471': {'city': 'Leipzig', 'forecast': {'tomorrow': 0, 'dayafter_to': 0, 'today': 0}, 'lat': 51.317, 'lon': 12.417}, '10739': {'city': 'Stuttgart', 'forecast': {'dayafter_to': 1, 'tomorrow': 0, 'today': 1}, 'lat': 48.833, 'lon': 9.2}, '10637': {'forecast': {'dayafter_to': 2, 'tomorrow': 1, 'today': 0}, 'city': 'Frankfurt/Main', 'lat': 50.05, 'lon': 8.6}, '10113': {'city': 'Norderney', 'forecast': {'today': 0, 'tomorrow': 0, 'dayafter_to': 0}, 'lat': 53.717, 'lon': 7.15}, '10389': {'forecast': {'dayafter_to': 0, 'tomorrow': 0, 'today': 0}, 'city': 'Berlin', 'lat': 52.517, 'lon': 13.417}, '10791': {'forecast': {'tomorrow': 0, 'dayafter_to': 1, 'today': 1}, 'city': 'Großer Arber', 'lat': 49.117, 'lon': 13.133}, '10555': {'city': 'Weimar', 'forecast': {'dayafter_to': 0, 'tomorrow': 0, 'today': 0}, 'lat': 50.983, 'lon': 11.317}, 'P0150': {'forecast': {'dayafter_to': 0, 'tomorrow': 0, 'today': 0}, 'city': 'Sankt Peter-Ording', 'lat': 54.317, 'lon': 8.683}, '10929': {'city': 'Konstanz', 'forecast': {'today': 1, 'dayafter_to': 1, 'tomorrow': 0}, 'lat': 47.683, 'lon': 9.183}, '10400': {'city': 'Düsseldorf', 'forecast': {'today': 0, 'dayafter_to': 0, 'tomorrow': 0}, 'lat': 51.3, 'lon': 6.767}, '10803': {'city': 'Freiburg', 'forecast': {'dayafter_to': 1, 'tomorrow': 0, 'today': 1}, 'lat': 48.017, 'lon': 7.833}, '10361': {'forecast': {'today': 0, 'tomorrow': 0, 'dayafter_to': 0}, 'city': 'Magdeburg', 'lat': 52.117, 'lon': 11.583}, '10454': {'city': 'Wernigerode', 'forecast': {'dayafter_to': 0, 'tomorrow': 0, 'today': 0}, 'lat': 51.85, 'lon': 10.767}, '10280': {'city': 'Neubrandenburg', 'forecast': {'today': 0, 'dayafter_to': 0, 'tomorrow': 0}, 'lat': 53.55, 'lon': 13.2}, '10519': {'forecast': {'tomorrow': 0, 'dayafter_to': 0, 'today': 1}, 'city': 'Bonn', 'lat': 50.733, 'lon': 7.183}, 'P0332': {'forecast': {'dayafter_to': 0, 'tomorrow': 0, 'today': 0}, 'city': 'Marienleuchte', 'lat': 54.467, 'lon': 11.167}, '10496': {'city': 'Cottbus', 'forecast': {'dayafter_to': 1, 'tomorrow': 0, 'today': 0}, 'lat': 51.783, 'lon': 14.317}, '10046': {'forecast': {'today': 0, 'dayafter_to': 0, 'tomorrow': 0}, 'city': 'Kiel', 'lat': 54.383, 'lon': 10.15}, '10020': {'forecast': {'dayafter_to': 0, 'tomorrow': 0, 'today': 0}, 'city': 'List auf Sylt', 'lat': 55.017, 'lon': 8.417}, '10091': {'city': 'Arkona', 'forecast': {'dayafter_to': 0, 'tomorrow': 0, 'today': 0}, 'lat': 54.683, 'lon': 13.433}, '10338': {'forecast': {'today': 0, 'tomorrow': 0, 'dayafter_to': 0}, 'city': 'Hannover', 'lat': 52.467, 'lon': 9.683}, '10865': {'city': 'München', 'forecast': {'dayafter_to': 1, 'tomorrow': 0, 'today': 1}, 'lat': 48.167, 'lon': 11.533}, '10268': {'city': 'Waren', 'forecast': {'today': 0, 'dayafter_to': 0, 'tomorrow': 0}, 'lat': 53.517, 'lon': 12.667}, '10427': {'forecast': {'dayafter_to': 0, 'tomorrow': 0, 'today': 0}, 'city': 'Kahler Asten', 'lat': 51.183, 'lon': 8.483}, '10616': {'forecast': {'today': 0, 'tomorrow': 0, 'dayafter_to': 0}, 'city': 'Hahn', 'lat': 49.95, 'lon': 7.267}, '10224': {'city': 'Bremen', 'forecast': {'today': 0, 'tomorrow': 0, 'dayafter_to': 0}, 'lat': 53.05, 'lon': 8.8}, '10655': {'city': 'Würzburg', 'forecast': {'dayafter_to': 1, 'tomorrow': 0, 'today': 1}, 'lat': 49.767, 'lon': 9.967}, 'P0175': {'city': 'Rostock', 'forecast': {'today': 0, 'tomorrow': 0, 'dayafter_to': 0}, 'lat': 54.083, 'lon': 12.133}, '10838': {'forecast': {'today': 1, 'dayafter_to': 1, 'tomorrow': 0}, 'city': 'Ulm', 'lat': 48.383, 'lon': 9.95}, '10776': {'forecast': {'today': 1, 'tomorrow': 0, 'dayafter_to': 1}, 'city': 'Regensburg', 'lat': 49.033, 'lon': 12.1}, '10438': {'city': 'Kassel', 'forecast': {'tomorrow': 0, 'dayafter_to': 0, 'today': 0}, 'lat': 51.3, 'lon': 9.45}, '10487': {'forecast': {'tomorrow': 0, 'dayafter_to': 1, 'today': 1}, 'city': 'Dresden', 'lat': 51.05, 'lon': 13.733}, '10961': {'city': 'Zugspitze', 'forecast': {'tomorrow': 1, 'dayafter_to': 1, 'today': 1}, 'lat': 47.417, 'lon': 10.983}, '10763': {'forecast': {'dayafter_to': 1, 'tomorrow': 0, 'today': 0}, 'city': 'Nürnberg', 'lat': 49.5, 'lon': 11.05}}
3 changes: 3 additions & 0 deletions tests/test_station.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ def test_is_valid_station_id_false(self):
def test_is_valid_station_id_empty_string(self):
self.assertFalse(dwdforecast.load_station_id(""))
self.assertFalse(dwdforecast.load_station_id(1))

def test_get_station_by_name(self):
self.assertEqual(dwdforecast.get_station_by_name("Ulm")[0], "10838")
18 changes: 18 additions & 0 deletions tests/test_uv_index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import unittest
from simple_dwd_weatherforecast import dwdforecast
from dummy_uv import parsed_data


class UvIndexTestCase(unittest.TestCase):
def setUp(self):
self.dwd_weather = dwdforecast.Weather("L821")
self.dwd_weather.uv_reports = parsed_data

def test_uv_today(self):
self.assertEqual(self.dwd_weather.get_uv_index(0), 0)

def test_uv_tomorrow(self):
self.assertEqual(self.dwd_weather.get_uv_index(1), 1)

def test_uv_dayafter_tomorrow(self):
self.assertEqual(self.dwd_weather.get_uv_index(2), 2)
5 changes: 4 additions & 1 deletion tests/test_weather.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

class WeatherInit(unittest.TestCase):
def setUp(self):
self.dwd_weather = dwdforecast.Weather("H889")
self.dwd_weather = dwdforecast.Weather("L821")
self.dwd_weather.forecast_data = parsed_data
self.dwd_weather.station_name = "BAD HOMBURG"

Expand All @@ -20,3 +20,6 @@ def test_init_with_number(self):
def test_init_with_no_id(self):
with self.assertRaises(TypeError) as _:
dwdforecast.Weather()

def test_uv_index(self):
self.assertEqual(self.dwd_weather.nearest_uv_index_station[0], "10637")

0 comments on commit 043cfb4

Please sign in to comment.