diff --git a/CHANGELOG.md b/CHANGELOG.md index e740d6f2..a70d6b2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 2023-02-04 +* Add SMHI (Sweden) as weather provider. Added by [hnnweb](https://github.com/mendhak/waveshare-epaper-display/pull/51) + ## 2023-01-29 * Add code to display on the [7.5 inch B version 2 screen](https://www.waveshare.com/product/displays/e-paper/epaper-1/7.5inch-e-paper-hat-b.htm) diff --git a/README.md b/README.md index 464c052f..5b27bcdb 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ The screen will display date, time, weather icon with high and low, and calendar - [Weather.gov (US)](#weathergov-us) - [Climacell (tomorrow.io)](#climacell-tomorrowio) - [VisualCrossing](#visualcrossing) + - [SMHI (Sweden)](#smhi-sweden) - [Pick a severe weather warning provider](#pick-a-severe-weather-warning-provider) - [Met Office (UK)](#met-office-uk-1) - [Weather.gov (US)](#weathergov-us-1) @@ -177,7 +178,11 @@ Register on [VisualCrossing](https://www.visualcrossing.com/). Under Account Det export VISUALCROSSING_APIKEY=XXXXXXXXXXXXXXXXXXXXXX +### SMHI (Sweden) +SMHI requires you to identify yourself. Just set your own email, + + export SMHI_SELF_IDENTIFICATION=you@example.com ## Pick a severe weather warning provider diff --git a/env.sh.sample b/env.sh.sample index ef781575..f450811e 100755 --- a/env.sh.sample +++ b/env.sh.sample @@ -15,6 +15,8 @@ # export WEATHER_MET_EIREANN=1 # Or, weather.gov self identification # export WEATHERGOV_SELF_IDENTIFICATION=you@example.com +# Or, SMHI self identification +# export SMHI_SELF_IDENTIFICATION=you@example.com # Your latitude and longitude to pass to weather providers export WEATHER_LATITUDE=51.5077 diff --git a/screen-weather-get.py b/screen-weather-get.py index e8c9ddfc..8537aa06 100644 --- a/screen-weather-get.py +++ b/screen-weather-get.py @@ -4,7 +4,7 @@ import sys import os import logging -from weather_providers import climacell, openweathermap, metofficedatahub, metno, meteireann, accuweather, visualcrossing, weathergov +from weather_providers import climacell, openweathermap, metofficedatahub, metno, meteireann, accuweather, visualcrossing, weathergov, smhi from alert_providers import metofficerssfeed, weathergovalerts from alert_providers import meteireann as meteireannalertprovider from utility import update_svg, configure_logging @@ -24,6 +24,7 @@ def format_weather_description(weather_description): weather_dict[2] = splits[1] if len(splits) > 1 else '' return weather_dict + def get_weather(location_lat, location_long, units): # gather relevant environment configs @@ -37,6 +38,7 @@ def get_weather(location_lat, location_long, units): visualcrossing_apikey = os.getenv("VISUALCROSSING_APIKEY") use_met_eireann = os.getenv("WEATHER_MET_EIREANN") weathergov_self_id = os.getenv("WEATHERGOV_SELF_IDENTIFICATION") + smhi_self_id = os.getenv("SMHI_SELF_IDENTIFICATION") if ( not climacell_apikey @@ -47,6 +49,7 @@ def get_weather(location_lat, location_long, units): and not visualcrossing_apikey and not use_met_eireann and not weathergov_self_id + and not smhi_self_id ): logging.error("No weather provider has been configured (Climacell, OpenWeatherMap, Weather.gov, MetOffice, AccuWeather, Met.no, Met Eireann, VisualCrossing...)") sys.exit(1) @@ -93,13 +96,19 @@ def get_weather(location_lat, location_long, units): logging.info("Getting weather from Climacell") weather_provider = climacell.Climacell(climacell_apikey, location_lat, location_long, units) + elif smhi_self_id: + logging.info("Getting weather from SMHI") + weather_provider = smhi.SMHI(smhi_self_id, location_lat, location_long, units) + weather = weather_provider.get_weather() logging.info("weather - {}".format(weather)) return weather + def format_alert_description(alert_message): return html.escape(alert_message) + def get_alert_message(location_lat, location_long): alert_message = "" alert_metoffice_feed_url = os.getenv("ALERT_METOFFICE_FEED_URL") @@ -149,7 +158,7 @@ def main(): alert_message = get_alert_message(location_lat, location_long) alert_message = format_alert_description(alert_message) - + output_dict = { 'LOW_ONE': "{}{}".format(str(round(weather['temperatureMin'])), degrees), 'HIGH_ONE': "{}{}".format(str(round(weather['temperatureMax'])), degrees), @@ -167,7 +176,7 @@ def main(): logging.debug("main() - {}".format(output_dict)) logging.info("Updating SVG") - + template_svg_filename = f'screen-template.{template_name}.svg' output_svg_filename = 'screen-output-weather.svg' update_svg(template_svg_filename, output_svg_filename, output_dict) diff --git a/weather_providers/smhi.py b/weather_providers/smhi.py new file mode 100644 index 00000000..525a65d2 --- /dev/null +++ b/weather_providers/smhi.py @@ -0,0 +1,137 @@ +import logging +from weather_providers.base_provider import BaseWeatherProvider + + +class SMHI(BaseWeatherProvider): + def __init__(self, smhi_self_id, location_lat, location_long, units): + self.smhi_self_id = smhi_self_id + self.location_lat = location_lat + self.location_long = location_long + self.units = units + + # Map SMHI icons to local icons + # Reference: https://opendata.smhi.se/apidocs/metfcst/parameters.html#parameter-wsymb + def get_icon_from_smhi_weathercode(self, weathercode, is_daytime): + + icon_dict = { + 1: "clear_sky_day" if is_daytime else "clearnight", # Clear sky + 2: "clear_sky_day" if is_daytime else "clearnight", # Nearly clear sky + 3: "few_clouds" if is_daytime else "partlycloudynight", # Variable cloudiness + 4: "scattered_clouds" if is_daytime else "partlycloudynight", # Halfclear sky + 5: "mostly_cloudy" if is_daytime else "mostly_cloudy_night", # Cloudy sky + 6: "overcast", # Overcast + 7: "climacell_fog", # Fog + 8: 'climacell_rain_light' if is_daytime else 'rain_night_light', # Light rain showers + 9: "climacell_rain" if is_daytime else "rain_night", # Moderate rain showers + 10: "climacell_rain_heavy" if is_daytime else "rain_night_heavy", # Heavy rain showers + 11: "thundershower_rain", # Thunderstorm + 12: "sleet", # Light sleet showers + 13: "sleet", # Moderate sleet showers + 14: "sleet", # Heavy sleet showers + 15: "climacell_snow_light", # Light snow showers + 16: "snow", # Moderate snow showers + 17: "snow", # Heavy snow showers + 18: 'climacell_rain_light' if is_daytime else 'rain_night_light', # Light rain + 19: "climacell_rain" if is_daytime else "rain_night", # Moderate rain + 20: "climacell_rain_heavy" if is_daytime else "rain_night_heavy", # Heavy rain + 21: "thundershower_rain", # Thunder + 22: "sleet", # Light sleet + 23: "sleet", # Moderate sleet + 24: "sleet", # Heavy sleet + 25: "climacell_snow_light", # Light snowfall + 26: "snow", # Moderate snowfall + 27: "snow", # Heavy snowfall + } + + icon = icon_dict[weathercode] + logging.debug( + "get_icon_by_weathercode({}) - {}" + .format(weathercode, icon)) + + return icon + + def get_description_from_smhi_weathercode(self, weathercode): + description_dict = { + 1: "Clear sky", + 2: "Nearly clear sky", + 3: "Variable cloudiness", + 4: "Halfclear sky", + 5: "Cloudy sky", + 6: "Overcast", + 7: "Fog", + 8: "Light rain showers", + 9: "Moderate rain showers", + 10: "Heavy rain showers", + 11: "Thunderstorm", + 12: "Light sleet showers", + 13: "Moderate sleet showers", + 14: "Heavy sleet showers", + 15: "Light snow showers", + 16: "Moderate snow showers", + 17: "Heavy snow showers", + 18: "Light rain", + 19: "Moderate rain", + 20: "Heavy rain", + 21: "Thunder", + 22: "Light sleet", + 23: "Moderate sleet", + 24: "Heavy sleet", + 25: "Light snowfall", + 26: "Moderate snowfall", + 27: "Heavy snowfall", + } + description = description_dict[weathercode] + + logging.debug( + "get_description_by_weathercode({}) - {}" + .format(weathercode, description)) + + return description.title() + + # Get weather from SMHI API + # https://opendata.smhi.se/apidocs/metfcst/get-forecast.html#get-point-forecast + # The API response is a complete forecast approximately 10 days ahead of the latest current forecast. + # All times in the answer given in UTC. + # Precipitation parameters have a distribution in time (a time interval) until the valid time for current data. + # The interval starts at the time step before. At the beginning of the forecast, the interval is one hour. + # Later in the forecast, the time interval increases (eg 3, 6 and 12 h). Unit remains mm / h. + # So, current hour is index 0, next hour is index 1 + # The API accepts 6 decimals in the lon/lat. With more decimals, it returns 404. + + def get_weather(self): + + url = ("https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/{}/lat/{}/data.json" + .format(self.location_long, self.location_lat)) + + headers = {"User-Agent": self.smhi_self_id} + + response_data = self.get_response_json(url, headers=headers) + logging.debug(response_data) + weather_data = response_data["timeSeries"] + logging.debug("get_weather() - {}".format(weather_data)) + + # Get the weather code of the first item. + for data in weather_data[0]["parameters"]: + if data["name"] == "Wsymb2": + weather_code = data["values"][0] + + daytime = self.is_daytime(self.location_lat, self.location_long) + + # { "temperatureMin": "2.0", "temperatureMax": "15.1", "icon": "mostly_cloudy", "description": "Cloudy with light breezes" } + # No Min or Max here. We just get the estimated temperature for the hour. + # Since we get the forecast for several hours and days, get the min/max for the next 12 hours? + weather = {} + temp_list = [] + + for item in range(0, 12): + for param in weather_data[item]['parameters']: + if param['name'] == 't': + temp = param['values'][0] + temp_list.append(temp) + + weather["temperatureMin"] = min(temp_list) + weather["temperatureMax"] = max(temp_list) + weather["icon"] = self.get_icon_from_smhi_weathercode(weather_code, daytime) + weather["description"] = self.get_description_from_smhi_weathercode(weather_code) + logging.debug(weather) + return weather