From 7b0a3dd5aca63343329eae189ca365b88229c22b Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 18 Apr 2023 14:31:25 +0200 Subject: [PATCH 1/2] Use WeatherAPI.com --- modules/weather.py | 168 ++++++++-------- modules/weather_api.py | 422 +++++++++++------------------------------ 2 files changed, 194 insertions(+), 396 deletions(-) diff --git a/modules/weather.py b/modules/weather.py index 087bfd0..6d6a8a8 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -1,5 +1,6 @@ from datetime import datetime, timezone, timedelta import io +import itertools import math from PIL import Image, ImageDraw, ImageFont @@ -55,10 +56,10 @@ class WeatherToolbarModule: weather = WeatherAPI().get_currently() infos = { - "Vent": "%d km/h" % weather["windSpeed"], + "Vent": "%d km/h" % weather["wind_kph"], "Humidité": "%d%%" % (weather["humidity"]), - "Indice UV": str(weather["uvIndex"]), - "Pression": "%d hPa" % weather["pressureSurfaceLevel"], + "Indice UV": str(int(weather["uv"])), + "Pression": "%d hPa" % weather["pressure_mb"], } txt = draw_format_infos(infos, width, height, fnt_R, fnt_B, self.label_margin, align_height=height/2, anchor="lm") @@ -82,21 +83,21 @@ class WeatherJumboCurrentModule: # current curweather = WeatherAPI().get_currently() - icon = Image.open("icons/" + WeatherAPI.get_icon(curweather["weatherCode"], current=True)).resize((height, height)) + icon = Image.open("icons/" + WeatherAPI.get_icon(curweather["condition"]["code"], night=not curweather["is_day"], current=True)).resize((height, height)) image.paste(icon, (int(width*self.middle_align - height), 0), icon) draw.text( (width*self.middle_align, height/3), - "%d˚ %s." % (math.trunc(curweather["temperature"]), curweather["summary"] if "summary" in curweather else WeatherAPI.get_description(curweather["weatherCode"])), + "%d˚ %s." % (math.trunc(curweather["temp_c"]), curweather["condition"]["text"]), fill="black", anchor="ld", font=fnt_Big ) - thisdayweather = WeatherAPI().get_daily()[0]["values"] + thisdayweather = list(itertools.islice(WeatherAPI().get_daily(), 1))[0] infos = { - "Ressentie": "%d˚" % curweather["temperatureApparent"], - "Minimale": "%d˚" % thisdayweather["temperatureMin"], - "Maximale": "%d˚" % thisdayweather["temperatureMax"], + "Ressentie": "%d˚" % curweather["feelslike_c"], + "Minimale": "%d˚" % thisdayweather["mintemp_c"], + "Maximale": "%d˚" % thisdayweather["maxtemp_c"], } txt = draw_format_infos(infos, (1-self.middle_align) * width, 20, fnt_R, fnt_B, 5, margin_bf_first=False, fill="black", anchor="lt") @@ -105,13 +106,12 @@ class WeatherJumboCurrentModule: # day fnt_Rig = ImageFont.truetype(config.fnt_R_path, 20) - dailyweather = WeatherAPI().get_daily() - dayweather = WeatherAPI().get_hourly() + dayweather = list(itertools.islice(WeatherAPI().get_hourly(), 2)) - if dayweather[0]["values"]["weatherCode"] != dayweather[1]["values"]["weatherCode"]: - display_longtext(draw, (width*self.middle_align, height/1.28), WeatherAPI.get_description(dayweather[1]["values"]["weatherCode"]) + " la prochaine heure.\n" + WeatherAPI.get_description(dailyweather[0]["values"]["weatherCodeMax"]) + " aujourd'hui.", fill="black", anchor="lm", font=fnt_Rig, maxwidth=(1-self.middle_align)*width) + if dayweather[0]["condition"]["text"] != dayweather[1]["condition"]["text"]: + display_longtext(draw, (width*self.middle_align, height/1.28), dayweather[1]["condition"]["text"] + " la prochaine heure.\n" + thisdayweather["condition"]["text"] + " aujourd'hui.", fill="black", anchor="lm", font=fnt_Rig, maxwidth=(1-self.middle_align)*width) else: - display_longtext(draw, (width*self.middle_align, height/1.28), WeatherAPI.get_description(dayweather[0]["values"]["weatherCodeMax"]) + " aujourd'hui", fill="black", anchor="lm", font=fnt_Rig, maxwidth=(1-self.middle_align)*width) + display_longtext(draw, (width*self.middle_align, height/1.28), thisdayweather["condition"]["text"] + " aujourd'hui", fill="black", anchor="lm", font=fnt_Rig, maxwidth=(1-self.middle_align)*width) return image @@ -133,12 +133,12 @@ class WeatherSunModule: draw = ImageDraw.Draw(image) fnt_R = ImageFont.truetype(config.fnt_R_path, int(height*0.7)) - thisdayweather = WeatherAPI().get_daily()[0]["values"] + thisdayweather = list(itertools.islice(WeatherAPI().get_daily(), 1))[0] import time infos = { - "sunrise": WeatherAPI().read_timestamp(thisdayweather["sunriseTime"]).strftime("%X"), - "sunset": WeatherAPI().read_timestamp(thisdayweather["sunsetTime"]).strftime("%X"), + "sunrise": thisdayweather["sunrise"], + "sunset": thisdayweather["sunset"], } align = start_align @@ -163,32 +163,35 @@ class WeatherRainModule: def draw_module(self, config, width, height): if datetime.now().hour >= 21: - thisdayweather = WeatherAPI().get_daily()[1]["values"] + thisdayweather = list(itertools.islice(WeatherAPI().get_daily(), 2))[1] else: - thisdayweather = WeatherAPI().get_daily()[0]["values"] + thisdayweather = list(itertools.islice(WeatherAPI().get_daily(), 1))[0] gauge = pygal.SolidGauge(config.pygal_config, half_pie=True, inner_radius=0.70, width=width, height=height*1.55, style=config.pygal_custom_style, show_legend=False, margin_top=-height*0.58, margin_left=1, margin_right=1) percent_formatter = lambda x: '{:.10g}%'.format(x) gauge.value_formatter = percent_formatter - if thisdayweather["precipitationProbabilityMax"] == 0 and thisdayweather["uvIndexMax"] > 4: - gauge.add('Index UV', [{'value': thisdayweather["uvIndexMax"] * 10}]) - icon_path = "wi-hot.png" - elif thisdayweather["precipitationProbabilityMax"] > 0: - gauge.add('Pluie', [{'value': thisdayweather["precipitationProbabilityMax"] + 1}]) + if thisdayweather["daily_chance_of_snow"] > 0: + gauge.add('Neige', [{'value': thisdayweather["daily_chance_of_snow"] + 1}]) + icon_path = "wi-snowflake-cold.png" + elif thisdayweather["daily_chance_of_rain"] > 0: + gauge.add('Pluie', [{'value': thisdayweather["daily_chance_of_rain"] + 1}]) icon_path = "wi-umbrella.png" - elif thisdayweather["windSpeedMax"] > 50: - gauge.add('Vent', [{'value': thisdayweather["windSpeedMax"], 'color': '#999'}]) + elif thisdayweather["uv"] > 4: + gauge.add('Index UV', [{'value': thisdayweather["uv"] * 10}]) + icon_path = "wi-hot.png" + elif thisdayweather["maxwind_kph"] > 50: + gauge.add('Vent', [{'value': thisdayweather["maxwind_kph"], 'color': '#999'}]) icon_path = "wi-strong-wind.png" - elif thisdayweather["visibilityMin"] < 10: - gauge.add('Visibilité', [{'value': thisdayweather["visibilityMin"] * 10, 'color': '#999'}]) + elif thisdayweather["avgvis_km"] < 10: + gauge.add('Visibilité', [{'value': thisdayweather["avgvis_km"] * 10, 'color': '#999'}]) icon_path = "wi-fog.png" - elif thisdayweather["cloudCoverAvg"] > 40: - gauge.add('Couverture nuageuse', [{'value': thisdayweather["cloudCoverAvg"], 'color': '#999'}]) - icon_path = "wi-cloudy.png" else: - gauge.add('Pluie', [{'value': thisdayweather["precipitationProbabilityMax"]}]) - icon_path = "wi-na.png" + gauge.add("Qualité de l'air", [{'value': thisdayweather["air_quality"]["gb-defra-index"] * 10}]) + if thisdayweather["air_quality"]["gb-defra-index"] >= 5: + icon_path = "wi-smog.png" + else: + icon_path = "wi-smoke.png" image = Image.open(io.BytesIO(gauge.render_to_png())) @@ -204,33 +207,33 @@ class WeatherTemperatureModule: self.limit_futur = 30 def draw_module(self, config, width, height): - thisdayweather = WeatherAPI().get_daily() - if datetime.now().hour >= 19 and thisdayweather[1]["precipitationProbabilityMax"] > thisdayweather[0]["precipitationProbabilityMax"]: + thisdayweather = list(itertools.islice(WeatherAPI().get_daily(), 2)) + if datetime.now().hour >= 19 and thisdayweather[1]["totalprecip_mm"] > thisdayweather[0]["totalprecip_mm"]: thisdayweather = thisdayweather[1] else: thisdayweather = thisdayweather[0] - hours_weather = WeatherAPI().get_hourly() + hours_weather = [h for h in WeatherAPI().get_hourly()] hourly_min = 0 hourly_max = 0 for h in hours_weather: - if hourly_min > h["values"]["temperature"]: - hourly_min = h["values"]["temperature"] - if hourly_max < h["values"]["temperature"]: - hourly_max = h["values"]["temperature"] + if hourly_min > h["temp_c"]: + hourly_min = h["temp_c"] + if hourly_max < h["temp_c"]: + hourly_max = h["temp_c"] - line_chart = pygal.Line(config.pygal_config, interpolate='cubic', width=width+10, height=height, inverse_y_axis=False, x_label_rotation=45, range=(hourly_min, hourly_max), secondary_range=(0,100) if thisdayweather["values"]["precipitationProbabilityMax"] > 0 else (0,10), **config.charts_opts) + line_chart = pygal.Line(config.pygal_config, interpolate='hermite', interpolation_parameters={'type': 'kochanek_bartels', 'b': -1, 'c': 1, 't': 1}, width=width+10, height=height, inverse_y_axis=False, x_label_rotation=45, range=(hourly_min, hourly_max), secondary_range=(0,100) if thisdayweather["totalprecip_mm"] + thisdayweather["totalsnow_cm"] > 0 else (0,10), **config.charts_opts) line_chart.value_formatter = lambda x: "%d" % x - line_chart.x_labels = [WeatherAPI().read_timestamp(d["time"]).strftime("%Hh") if WeatherAPI().read_timestamp(d["time"]).hour % 2 == 0 else "" for d in hours_weather[:self.limit_futur]] + line_chart.x_labels = [WeatherAPI().read_timestamp(d["time_epoch"]).strftime("%Hh") if WeatherAPI().read_timestamp(d["time_epoch"]).hour % 2 == 0 else "" for d in hours_weather[:self.limit_futur]] - line_chart.add('Températures', [d["values"]["temperature"] for d in hours_weather[:self.limit_futur]], show_dots=False) + line_chart.add('Températures', [d["temp_c"] for d in hours_weather[:self.limit_futur]], show_dots=False) - if thisdayweather["values"]["precipitationProbabilityMax"] > 0: - line_chart.add('Précipitations', [d["values"]["precipitationProbability"] for d in hours_weather[:self.limit_futur]], secondary=True, show_dots=False, fill=True if hourly_min == 0 else False) + if thisdayweather["totalprecip_mm"] + thisdayweather["totalsnow_cm"] > 0: + line_chart.add('Précipitations', [d["chance_of_rain"] + d["chance_of_snow"] for d in hours_weather[:self.limit_futur]], secondary=True, show_dots=False, fill=True if hourly_min == 0 else False) else: - line_chart.add('Index UV', [d["values"]["uvIndex"] for d in hours_weather[:self.limit_futur]], secondary=True, show_dots=False) + line_chart.add('Index UV', [d["uv"] for d in hours_weather[:self.limit_futur]], secondary=True, show_dots=False) img = Image.open(io.BytesIO(line_chart.render_to_png())) draw = ImageDraw.Draw(img) @@ -255,60 +258,60 @@ class WeeklyWeatherModule: fnt_R = ImageFont.truetype(config.fnt_R_path, 14) fnt_B = ImageFont.truetype(config.fnt_RB_path, 14) - weekweather = WeatherAPI().get_daily() + weekweather = [d for d in WeatherAPI().get_daily()] nbdays = len(weekweather[self.first_day:self.first_day+self.limit_futur]) day_size = min(40, int(height / (nbdays + 1))) display_longtext(draw, (day_size + (width - day_size)/2, day_size / 2 + 5), - weekweather["summary"] if "summary" in weekweather else "", + weekweather["summary"] if "summary" in weekweather else "", fill="black", anchor="mm", font=fnt_B, maxwidth=width - day_size ) temp = [] for day in weekweather[self.first_day:self.first_day+self.limit_futur]: - temp.append(day["values"]["temperatureMin"]) - temp.append(day["values"]["temperatureMax"]) + temp.append(day["mintemp_c"]) + temp.append(day["maxtemp_c"]) t_min = min(temp) t_max = max(temp) t_scale = (width - day_size - 30) / (t_max - t_min) i = 1 for day in weekweather[self.first_day:self.first_day+self.limit_futur]: - icon = Image.open("icons/" + WeatherAPI.get_icon(day["values"]["weatherCodeMax"])).resize((day_size, day_size)) + icon = Image.open("icons/" + WeatherAPI.get_icon(day["condition"]["code"])).resize((day_size, day_size)) image.paste(icon, (0, i * day_size), icon) draw.text( - (15 + 2 + day_size + int((day["values"]["temperatureMin"]-t_min)*t_scale), i*day_size + 4), - "%d˚" % math.trunc(day["values"]["temperatureMax"]), + (15 + 2 + day_size + int((day["mintemp_c"]-t_min)*t_scale), i*day_size + 4), + "%d˚" % math.trunc(day["mintemp_c"]), fill="black", anchor="rt", font=fnt_R ) - summary_size = fnt_R.getsize(WeatherAPI().read_timestamp(day["time"]).strftime("%a") + " : " + (day["values"]["summary"] if "summary" in day["values"] else WeatherAPI.get_description(day["values"]["weatherCodeMax"])))[0] + summary_size = fnt_R.getsize(day["date"].strftime("%a") + " : " + day["condition"]["text"])[0] draw.text( (day_size + (width - day_size - summary_size) / 2, (i + 1) * day_size - 6), - WeatherAPI().read_timestamp(day["time"]).strftime("%a") + " : ", + day["date"].strftime("%a") + " : ", fill="#666", anchor="ls", font=fnt_R ) draw.text( (day_size + (width - day_size + summary_size) / 2, (i + 1) * day_size - 6), - day["values"]["summary"] if "summary" in day["values"] else WeatherAPI.get_description(day["values"]["weatherCodeMax"]), + day["condition"]["text"], fill="black", anchor="rs", font=fnt_R ) draw.text( - (day_size + int((day["values"]["temperatureMax"]-t_min)*t_scale) + 2, i * day_size + 4), - "%d˚" % math.trunc(day["values"]["temperatureMax"]), + (day_size + int((day["maxtemp_c"]-t_min)*t_scale) + 2, i * day_size + 4), + "%d˚" % math.trunc(day["maxtemp_c"]), fill="black", anchor="lt", font=fnt_R ) try: draw.rounded_rectangle( - (15 + day_size + int((day["values"]["temperatureMin"]-t_min)*t_scale),i*day_size + 4,day_size + int((day["values"]["temperatureMax"]-t_min)*t_scale),i*day_size + 14), + (15 + day_size + int((day["mintemp_c"]-t_min)*t_scale),i*day_size + 4,day_size + int((day["maxtemp_c"]-t_min)*t_scale),i*day_size + 14), radius=5, fill="black") except AttributeError: draw.rectangle( - (15 + day_size + int((day["values"]["temperatureMin"]-t_min)*t_scale),i*day_size + 4,day_size + int((day["values"]["temperatureMax"]-t_min)*t_scale),i*day_size + 14), + (15 + day_size + int((day["mintemp_c"]-t_min)*t_scale),i*day_size + 4,day_size + int((day["maxtemp_c"]-t_min)*t_scale),i*day_size + 14), fill="black") i += 1 @@ -320,30 +323,29 @@ class WeatherAlerts: def gen_alerts(self): alerts = [] - if WeatherAPI().has_alerts(): - for alert in WeatherAPI().get_alerts(): - if alert["severity"] == "watch" or alert["title"].startswith("Moderate"): - icon = "wi-small-craft-advisory.png" - elif alert["severity"] == "warning": - icon = "wi-gale-warning.png" - else: - icon = None + for alert in WeatherAPI().get_alerts(): + if alert["severity"] == "Moderate": + icon = "wi-small-craft-advisory.png" + elif alert["severity"] != "Moderate": + icon = "wi-gale-warning.png" + else: + icon = None - startTime = WeatherAPI().read_timestamp(alert["time"]) - endTime = WeatherAPI().read_timestamp(alert["expires"]) - # Show alert timing if under a day - if startTime.hour != endTime.hour: - subtitle = startTime.strftime(("%x " if startTime.day != datetime.now().day else "") + "%X") + " - " + endTime.strftime(("%x " if startTime.day != endTime.day else "") + "%X") - elif startTime.day != datetime.now().day: - subtitle = startTime.strftime("%x") - else: - subtitle = "" + startTime = WeatherAPI().read_timestamp(alert["effective"]) + endTime = WeatherAPI().read_timestamp(alert["expires"]) + # Show alert timing if under a day + if startTime.hour != endTime.hour: + subtitle = startTime.strftime(("%x " if startTime.day != datetime.now().day else "") + "%X") + " - " + endTime.strftime(("%x " if startTime.day != endTime.day else "") + "%X") + elif startTime.day != datetime.now().day: + subtitle = startTime.strftime("%x") + else: + subtitle = "" - alerts.append({ - "icon": icon, - "title": alert["title"], - "subtitle": subtitle, - "description": alert["description"], - }) + alerts.append({ + "icon": icon, + "title": alert["headline"], + "subtitle": subtitle, + "description": alert["desc"], + }) return alerts diff --git a/modules/weather_api.py b/modules/weather_api.py index 75294d8..503c594 100644 --- a/modules/weather_api.py +++ b/modules/weather_api.py @@ -1,4 +1,5 @@ from datetime import datetime, timedelta, timezone +import itertools import json import os import urllib.error @@ -6,33 +7,41 @@ import urllib.parse import urllib.request from zoneinfo import ZoneInfo -class TomorrowAPI: +class WeatherAPI: - def __init__(self, apikey=None, gps=None, opts={"units": "metric"}): - self.apikey = apikey or os.environ["TOMORROWAPIKEY"] - self.baseurl = "https://api.tomorrow.io/v4" + def __init__(self, apikey=None, gps=None, opts={"lang": "fr", "units": "metric"}): + self.apikey = apikey or os.environ["WEATHERAPIKEY"] + self.baseurl = "http://api.weatherapi.com/v1" self.default_gps = gps or ("GPS" in os.environ and os.environ["GPS"]) or "48.8127,2.3437" self.opts = opts self._cached_file = ".weather-%s-%s.cache" - def get_weather(self, apitype="forecast", gps=None): + def get_weather(self, apitype="forecast", gps=None, **params): if gps is None: gps = self.default_gps + cached_filename = self._cached_file % (apitype + ("" if "dt" not in params else "-{}".format(params["dt"])), gps) # Read the mod time statinfo = None try: - statinfo = os.stat(self._cached_file % (apitype, gps)) + statinfo = os.stat(cached_filename) except: pass + params["q"] = gps + params["key"] = str(self.apikey) + params["alerts"] = "yes" + params["aqi"] = "yes" + for k in self.opts: + params[k] = self.opts[k] + if statinfo is None or datetime.fromtimestamp(statinfo.st_mtime, tz=timezone.utc) + timedelta(hours=1) < datetime.now(tz=timezone.utc): # Do the request and save it try: - with urllib.request.urlopen(self.baseurl + "/weather/" + apitype + "?location=" + gps + "&apikey=" + str(self.apikey) + "&" + ("&".join([opt+"="+self.opts[opt] for opt in self.opts]))) as f: - with open(self._cached_file % (apitype, gps), 'wb') as fd: + with urllib.request.urlopen(self.baseurl + "/" + apitype + ".json?" + urllib.parse.urlencode(params)) as f: + with open(cached_filename, 'wb') as fd: fd.write(f.read()) except ConnectionResetError: pass @@ -42,7 +51,7 @@ class TomorrowAPI: # Retrieve cached data res = {} - with open(self._cached_file % (apitype, gps)) as f: + with open(cached_filename) as f: res = json.load(f) return res @@ -54,366 +63,153 @@ class TomorrowAPI: return "wi-day-sunny.png" else: return "wi-night-clear.png" - elif icon == 1100: + elif icon == 1003: + if not night: + return "wi-day-cloudy.png" + else: + return "wi-night-alt-cloudy.png" + elif icon == 1006: + return "wi-cloudy.png" + elif icon == 1009: if not night: return "wi-day-sunny-overcast.png" else: return "wi-night-alt-partly-cloudy.png" - elif icon == 1101: + elif icon == 1030 or icon == 1072: if not night: - return "wi-day-cloudy-high.png" - else: - return "wi-night-partly-cloudy.png" - elif icon == 1102: - return "wi-cloud.png" - elif icon == 1001: - return "wi-cloudy.png" - elif icon == 1103: - if not night: - return "wi-day-cloudy-high.png" - else: - return "night-cloud-high.png" - elif icon == 2000: - return "wi-fog.png" - elif icon == 2100 or icon == 2101 or icon == 2102 or icon == 2103 or icon == 2106 or icon == 2107 or icon == 2108: - if not night: - return "wi-day-fog.png" + return "wi-day-haze.png" else: return "wi-night-fog.png" - elif icon == 4000: - return "wi-sprinkle.png" - elif icon == 4000: - return "wi-sprinkle.png" - elif icon == 4200: - return "wi-showers.png" - elif icon == 4001: - return "wi-rain.png" - elif icon == 4201: - return "wi-showers.png" - elif icon == 4203 or icon == 4204 or icon == 4205 or icon == 4213 or icon == 4214 or icon == 4215 or icon == 4209 or icon == 4208 or icon == 4210 or icon == 4211 or icon == 4202 or icon == 4212: + elif icon == 1063: if not night: return "wi-day-rain.png" else: return "wi-night-alt-rain.png" - elif icon == 5001 or icon == 5100 or icon == 5000 or icon == 5101: - return "wi-snow.png" - elif icon == 5115 or icon == 5116: + elif icon == 1066: + if not night: + return "wi-day-snow.png" + else: + return "wi-night-alt-snow.png" + elif icon == 1069 or icon == 1204: if not night: return "wi-day-sleet.png" else: - return "wi-night-sleet.png" - elif icon == 5122 or icon == 5110 or icon == 5108 or icon == 5114 or icon == 5112: + return "wi-night-alt-sleet.png" + elif icon == 1087: + if not night: + return "wi-day-lightning.png" + else: + return "wi-night-alt-lightning.png" + elif icon == 1114 or icon == 1219 or icon == 1222 or icon == 1258: + return "wi-snow.png" + elif icon == 1117 or icon == 1225: + return "wi-snow-wind.png" + elif icon == 1135 or icon == 1147: + return "wi-fog.png" + elif icon == 1150 or icon == 1153: + return "wi-sprinkle.png" + elif icon == 1168 or icon == 1171 or icon == 1198 or icon == 1201: + return "wi-snowflake-cold.png" + elif icon == 1180 or icon == 1183 or icon == 1186: + if not night: + return "wi-day-rain.png" + else: + return "wi-night-alt-rain.png" + elif icon == 1189 or icon == 1192 or icon == 1195: + return "wi-rain.png" + elif icon == 1240 or icon == 1243: + return "wi-showers.png" + elif icon == 1246: + return "wi-tsunami.png" + elif icon == 1207 or icon == 1249 or icon == 1252: return "wi-sleet.png" - elif icon == 5103 or icon == 5104 or icon == 5105 or icon == 5106 or icon == 5107 or icon == 5119 or icon == 5120 or icon == 5121: + elif icon == 1210 or icon == 1213 or icon == 1216 or icon == 1255: if not night: return "wi-day-snow.png" else: return "wi-night-snow.png" - elif icon == 6000 or icon == 6200 or icon == 6001 or icon == 6201 or icon == 6204 or icon == 6206 or icon == 6212 or icon == 6220 or icon == 6222: - return "wi-sprinkle.png" - elif icon == 6003 or icon == 6002 or icon == 6004 or icon == 6205 or icon == 6203 or icon == 6209 or icon == 6213 or icon == 6214 or icon == 6215 or icon == 6207 or icon == 6202 or icon == 6208: - if not night: - return "wi-day-sprinkle.png" - else: - return "wi-night-sprinkle.png" - elif icon == 7102 or icon == 7000 or icon == 7101 or icon == 7105 or icon == 7115 or icon == 7117 or icon == 7106 or icon == 7103: + elif icon == 1237: return "wi-hail.png" - elif icon == 7110 or icon == 7111 or icon == 7112 or icon == 7108 or icon == 7107 or icon == 7109 or icon == 7114 or icon == 7116: + elif icon == 1261 or icon == 1264: if not night: return "wi-day-hail.png" else: return "wi-night-hail.png" - elif icon == 8000: - return "wi-thunderstorm.png" - elif icon == 8001 or icon == 8003 or icon == 8002: + elif icon == 1273 or icon == 1276: if not night: return "wi-day-thunderstorm.png" else: return "wi-night-thunderstorm.png" + elif icon == 1279 or icon == 1282: + if not night: + return "wi-day-snow-thunderstorm.png" + else: + return "wi-night-snow-thunderstorm.png" else: return "wi-alien.png" - def get_description(icon, night=False): - if icon == 1000: - if not night: - return "Ensoleillé" - else: - return "Temps clair" - elif icon == 1100: - return "Dégagé" - elif icon == 1101: - return "Partiellement nuageux" - elif icon == 1102: - return "Plutôt nuageux" - elif icon == 1001: - return "Nuageux" - elif icon == 1103: - return "Dégagé et nuageux" - elif icon == 2000: - return "Brouillard" - elif icon == 2100: - return "Brouillard léger" - elif icon == 2101: - return "Brouillard léger et ciel dégagé" - elif icon == 2102: - return "Brouillard léger et ciel partiellement nuageux" - elif icon == 2103: - return "Brouillard léger et ciel plutôt nuageux" - elif icon == 2106: - return "Brouillard et ciel dégagé" - elif icon == 2107: - return "Brouillard et ciel partiellement nuageux" - elif icon == 2108: - return "Brouillard et ciel plutôt nuageux" - elif icon == 4000: - return "Bruine" - elif icon == 4200: - return "Pluie légère" - elif icon == 4001: - return "Pluie" - elif icon == 4201: - return "Forte pluie" - elif icon == 4203: - return "Bruine et ciel dégagé" - elif icon == 4204: - return "Bruine et ciel partiellement nuageux" - elif icon == 4205: - return "Bruine et ciel plutôt nuageux" - elif icon == 4213: - return "Averse et ciel dégagé" - elif icon == 4214: - return "Averse et ciel partiellement nuageux" - elif icon == 4215: - return "Averse et ciel plutôt nuageux" - elif icon == 4209: - return "Pluie et ciel dégagé" - elif icon == 4208: - return "Pluie et ciel partiellement nuageux" - elif icon == 4210: - return "Pluie et ciel plutôt nuageux" - elif icon == 4211: - return "Pluie forte et ciel dégagé" - elif icon == 4202: - return "Pluie forte et ciel partiellement nuageux" - elif icon == 4212: - return "Pluie forte et ciel plutôt nuageux" - elif icon == 5001: - return "Quelques flocons" - elif icon == 5100: - return "Neige légère" - elif icon == 5000: - return "Neige" - elif icon == 5101: - return "Tempête de neige" - elif icon == 5115: - return "Quelques flocons et ciel dégagé" - elif icon == 5116: - return "Quelques flocons et ciel partiellement nuageux" - elif icon == 5117: - return "Quelques flocons et ciel plutôt nuageux" - elif icon == 5122: - return "Bruine et quelques flocons" - elif icon == 5102: - return "Neige légère et ciel dégagé" - elif icon == 5103: - return "Neige légère et ciel partiellement nuageux" - elif icon == 5104: - return "Neige légère et ciel plutôt nuageux" - elif icon == 5105: - return "Neige et ciel dégagé" - elif icon == 5106: - return "Neige et ciel partiellement nuageux" - elif icon == 5107: - return "Neige et ciel plutôt nuageux" - elif icon == 5119: - return "Neige abondante et ciel dégagé" - elif icon == 5120: - return "Neige abondante et ciel partiellement nuageux" - elif icon == 5121: - return "Neige abondante et ciel plutôt nuageux" - elif icon == 5110: - return "Bruine et neige" - elif icon == 5108: - return "Pluie et neige" - elif icon == 5114: - return "Neige et pluie verglaçante" - elif icon == 5112: - return "Neige et grèle" - elif icon == 6000: - return "Bruine verglaçante" - elif icon == 6200: - return "Légère bruine verglaçante" - elif icon == 6001: - return "Pluie verglaçante" - elif icon == 6201: - return "Pluie forte verglaçante" - elif icon == 6003: - return "Bruine verglaçante et ciel dégagé" - elif icon == 6002: - return "Bruine verglaçante et ciel partiellement nuageux" - elif icon == 6004: - return "Bruine verglaçante et ciel plutôt nuageux" - elif icon == 6204: - return "Bruine verglaçante et bruine" - elif icon == 6206: - return "Bruine verglaçante et pluie légère" - elif icon == 6205: - return "Averse verglaçante et ciel dégagé" - elif icon == 6203: - return "Averse verglaçante et ciel partiellement nuageux" - elif icon == 6209: - return "Averse verglaçante et ciel plutôt nuageux" - elif icon == 6213: - return "Pluie verglaçante et ciel dégagé" - elif icon == 6214: - return "Pluie verglaçante et ciel partiellement nuageux" - elif icon == 6215: - return "Pluie verglaçante et ciel plutôt nuageux" - elif icon == 6212: - return "Pluie verglaçante et bruine" - elif icon == 6220: - return "Pluie verglaçante et pluie légère" - elif icon == 6222: - return "Averses et pluie verglaçante" - elif icon == 6207: - return "Pluie forte verglaçante et ciel dégagé" - elif icon == 6202: - return "Pluie forte verglaçante et ciel partiellement nuageux" - elif icon == 6208: - return "Pluie forte verglaçante et ciel plutôt nuageux" - elif icon == 7000: - return "Grèle" - elif icon == 7102: - return "Grèle légère" - elif icon == 7101: - return "Forte grèle" - elif icon == 7110: - return "Grèle légère et ciel dégagé" - elif icon == 7111: - return "Grèle légère et ciel partiellement nuageux" - elif icon == 7112: - return "Grèle légère et ciel plutôt nuageux" - elif icon == 7108: - return "Grèle et ciel dégagé" - elif icon == 7107: - return "Grèle et ciel partiellement nuageux" - elif icon == 7109: - return "Grèle et ciel plutôt nuageux" - elif icon == 7113: - return "Grèle forte et ciel dégagé" - elif icon == 7114: - return "Grèle forte et ciel partiellement nuageux" - elif icon == 7116: - return "Grèle forte et ciel plutôt nuageux" - elif icon == 7105: - return "Bruine et grèle" - elif icon == 7115: - return "Pluie légère et grèle" - elif icon == 7117: - return "Pluie et grèle" - elif icon == 7106: - return "Pluie verglaçante et grèle" - elif icon == 7103: - return "Pluie verglaçante et grèle forte" - elif icon == 8000: - return "Orageux" - elif icon == 8001: - return "Orage et ciel dégagé" - elif icon == 8003: - return "Orage et ciel partiellement nuageux" - elif icon == 8002: - return "Orage et ciel plutôt nuageux" - else: - return "Invasion d'aliens ?" - - def get_moon_icon(self, day=0): - moon_phase = self.get_daily()["data"][day]["moonPhase"] + moon_phase = list(itertools.islice(self.get_daily(), 1))[day]["moon_phase"] - if moon_phase < 0.035: + if moon_phase == "New Moon": return "wi-moon-alt-new.png" - elif moon_phase < 0.071: - return "wi-moon-alt-waxing-crescent-1.png" - elif moon_phase < 0.107: - return "wi-moon-alt-waxing-crescent-2.png" - elif moon_phase < 0.142: - return "wi-moon-alt-waxing-crescent-3.png" - elif moon_phase < 0.178: + elif moon_phase == "Waxing Crescent": return "wi-moon-alt-waxing-crescent-4.png" - elif moon_phase < 0.214: - return "wi-moon-alt-waxing-crescent-5.png" - elif moon_phase < 0.25: - return "wi-moon-alt-waxing-crescent-6.png" - elif moon_phase < 0.285: + elif moon_phase == "First Quarter": return "wi-moon-alt-first-quarter.png" - elif moon_phase < 0.321: - return "wi-moon-alt-waxing-gibbous-1.png" - elif moon_phase < 0.357: - return "wi-moon-alt-waxing-gibbous-2.png" - elif moon_phase < 0.392: - return "wi-moon-alt-waxing-gibbous-3.png" - elif moon_phase < 0.428: + elif moon_phase == "Waxing Gibbous": return "wi-moon-alt-waxing-gibbous-4.png" - elif moon_phase < 0.464: - return "wi-moon-alt-waxing-gibbous-5.png" - elif moon_phase < 0.5: - return "wi-moon-alt-waxing-gibbous-6.png" - elif moon_phase < 0.535: + elif moon_phase == "Full Moon": return "wi-moon-alt-full.png" - elif moon_phase < 0.571: - return "wi-moon-alt-waning-gibbous-1.png" - elif moon_phase < 0.607: - return "wi-moon-alt-waning-gibbous-2.png" - elif moon_phase < 0.642: - return "wi-moon-alt-waning-gibbous-3.png" - elif moon_phase < 0.678: + elif moon_phase == "Waning Gibbous": return "wi-moon-alt-waning-gibbous-4.png" - elif moon_phase < 0.714: - return "wi-moon-alt-waning-gibbous-5.png" - elif moon_phase < 0.75: - return "wi-moon-alt-waning-gibbous-6.png" - elif moon_phase < 0.785: + elif moon_phase == "Last Quarter": return "wi-moon-alt-third-quarter.png" - elif moon_phase < 0.821: - return "wi-moon-alt-waning-crescent-1.png" - elif moon_phase < 0.857: - return "wi-moon-alt-waning-crescent-2.png" - elif moon_phase < 0.892: - return "wi-moon-alt-waning-crescent-3.png" - elif moon_phase < 0.928: - return "wi-moon-alt-waning-crescent-4.png" - elif moon_phase < 0.964: - return "wi-moon-alt-waning-crescent-5.png" else: - return "wi-moon-alt-waning-crescent-6.png" + return "wi-moon-alt-waning-crescent-3.png" def get_currently(self, *args, **kwargs): - return self.get_weather("realtime", *args, **kwargs)["data"]["values"] + return self.get_weather("current", *args, **kwargs)["current"] + def get_forecast(self, *args, **kwargs): + for i in [0, 1, 2, 3]: + enddt = datetime.now() + timedelta(days=i) + v = self.get_weather(*args, **kwargs, dt=enddt.strftime("%Y-%m-%d"))["forecast"]["forecastday"][0] + v["day"]["date"] = enddt + yield v + def get_hourly(self, *args, **kwargs): - return self.get_weather(*args, **kwargs)["timelines"]["hourly"] - + now = datetime.now().astimezone(ZoneInfo("Europe/Paris")) + for d in self.get_forecast(*args, **kwargs): + for h in d["hour"]: + if d["day"]["date"].day == now.day and now > self.read_timestamp(h["time_epoch"]) + timedelta(hours=1): + continue + yield h def get_daily(self, *args, **kwargs): - return self.get_weather(*args, **kwargs)["timelines"]["daily"] - - def has_alerts(self, *args, **kwargs): - return "alerts" in self.get_weather(*args, **kwargs) and len(self.get_weather(*args, **kwargs)["alerts"]) > 0 + for d in self.get_forecast(*args, **kwargs): + yield { + **d["day"], + **d["astro"] + } def get_alerts(self, *args, **kwargs): - return self.get_weather(*args, **kwargs)["alerts"] + for i in [0, 1, 2, 3]: + enddt = datetime.now() + timedelta(days=i) + for a in self.get_weather(*args, **kwargs, dt=enddt.strftime("%Y-%m-%d"))["alerts"]["alert"]: + yield a def read_timestamp(self, timestamp, *args, **kwargs): PARIS = ZoneInfo("Europe/Paris") - return datetime.fromisoformat(timestamp.replace("Z", "+00:00")).astimezone(PARIS) + return datetime.fromtimestamp(timestamp, tz=timezone.utc).astimezone(PARIS) -WeatherAPI = TomorrowAPI +#WeatherAPI = TomorrowAPI if __name__ == '__main__': - dsa = TomorrowAPI() - print(dsa.get_currently()) - print(dsa.get_daily()) + dsa = WeatherAPI() + print(json.dumps(dsa.get_currently())) + #print(json.dumps([d for d in dsa.get_daily()][0])) From e8fd423e5f8dc0c178c3d1b58bdd2b27ff3ca428 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 18 Apr 2023 15:17:17 +0200 Subject: [PATCH 2/2] Add air quality indicators --- main.py | 6 +++- modules/weather.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 15f2d49..4c27d3f 100644 --- a/main.py +++ b/main.py @@ -98,7 +98,11 @@ def main(only_on_coming_evt=False, ignore_module=[], force_coming_event=True, ex if occuped_space < 250: # weekly weather from modules.weather import WeeklyWeatherModule - shape.append(WidgetPlacement(WeeklyWeatherModule, size=(int(480/1.6), 275), position=(480-int(480/1.6), NEXT_STOP_Y + occuped_space + (5 if occuped_space else 0)))) + shape.append(WidgetPlacement(WeeklyWeatherModule, size=(int(480/1.6), 165), position=(480-int(480/1.6), NEXT_STOP_Y + occuped_space + (5 if occuped_space else 0)))) + + # air quality + from modules.weather import WeatherAirQualityModule + shape.append(WidgetPlacement(WeatherAirQualityModule, size=(int(480/1.6), 100), position=(480-int(480/1.6), NEXT_STOP_Y + 165 + occuped_space + (5 if occuped_space else 0)))) # RATP weather major_lines = ["M7", "M14", "RB", "TT3A"] diff --git a/modules/weather.py b/modules/weather.py index 6d6a8a8..3297115 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -41,6 +41,44 @@ def draw_format_infos(infos, width, height, fnt_R, fnt_B, label_margin, align_he return image +def draw_format_array(infos, width, height, fnt_R, fnt_B, label_margin, align_height=0, margin_bf_first=True, **kwargs): + image = Image.new('RGBA', (int(width), int(height))) + draw = ImageDraw.Draw(image) + + nb_infos = len(infos.keys()) + size = 0 + title_hsize = 0 + text_hsize = 0 + for k,v in infos.items(): + ksize,ksizeH = fnt_R.getsize(k) + vsize,vsizeH = fnt_B.getsize(v) + if title_hsize < ksizeH: + title_hsize = ksizeH + if text_hsize < vsizeH: + text_hsize = vsizeH + size += max(ksize,vsize) + size += label_margin + + margin = (width - size) / nb_infos + align = 0 + if margin_bf_first: + align += margin / 2 + for k,v in infos.items(): + size = max(fnt_R.getsize(k)[0],fnt_B.getsize(v)[0]) + draw.text( + (align + (0 if "anchor" not in kwargs or kwargs["anchor"][0] != "m" else size/2), align_height-title_hsize/2), + k, + font=fnt_R, **kwargs + ) + draw.text( + (align + (0 if "anchor" not in kwargs or kwargs["anchor"][0] != "m" else size/2), align_height+text_hsize/2), + v, + font=fnt_B, **kwargs + ) + align += size + margin + + return image + class WeatherToolbarModule: def __init__(self): @@ -349,3 +387,37 @@ class WeatherAlerts: }) return alerts + + +class WeatherAirQualityModule: + + def __init__(self): + self.label_margin = 3 + + def draw_module(self, config, width, height): + image = Image.new('RGB', (width, height), 'white') + + draw = ImageDraw.Draw(image) + fnt_R = ImageFont.truetype(config.fnt_R_path, 16) + fnt_B = ImageFont.truetype(config.fnt_RB_path, 20) + + weather = WeatherAPI().get_currently() + + infos1 = { + "CO": "%d μg/m³" % weather["air_quality"]["co"], + "O3": "%d μg/m³" % (weather["air_quality"]["o3"]), + "NO2": "%d μg/m³" % (weather["air_quality"]["no2"]), + } + infos2 = { + "SO2": "%d μg/m³" % (weather["air_quality"]["so2"]), + "PM2.5": "%d μg/m³" % (weather["air_quality"]["pm2_5"]), + "PM10": "%d μg/m³" % (weather["air_quality"]["pm10"]), + } + + txt = draw_format_array(infos1, width, height/2, fnt_R, fnt_B, self.label_margin, align_height=height/4, anchor="mm", fill="black") + image.paste(txt, (0,0), txt) + + txt = draw_format_array(infos2, width, height/2, fnt_R, fnt_B, self.label_margin, align_height=height/4, anchor="mm", fill="black") + image.paste(txt, (0,int(height/2)), txt) + + return image