from datetime import datetime, timezone, timedelta import io import itertools import math import os.path from PIL import Image, ImageDraw, ImageFont import pygal from . import display_longtext from .weather_api import WeatherAPI def draw_format_infos(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 for k,v in infos.items(): size += fnt_R.getsize(k)[0] size += label_margin size += fnt_B.getsize(v)[0] margin = (width - size) / nb_infos align = 0 if margin_bf_first: align += margin / 2 for k,v in infos.items(): draw.text( (align, align_height), k, font=fnt_R, **kwargs ) align += fnt_R.getsize(k)[0] + label_margin draw.text( (align, align_height), v, font=fnt_B, **kwargs ) align += fnt_B.getsize(v)[0] + margin 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): self.label_margin = 3 def draw_module(self, config, width, height): image = Image.new('RGB', (width, height), 'black') 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() infos = { "Vent": "%d km/h" % weather["wind_kph"], "Humidité": "%d%%" % (weather["humidity"]), "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") image.paste(txt, (0,0), txt) return image class WeatherJumboCurrentModule: def __init__(self): self.middle_align = 1/3 def draw_module(self, config, width, height): image = Image.new('RGB', (width, height), '#fff') draw = ImageDraw.Draw(image) fnt_Big = ImageFont.truetype(config.fnt_RB_path, 33) fnt_R = ImageFont.truetype(config.fnt_R_path, 16) fnt_B = ImageFont.truetype(config.fnt_RB_path, 16) # current curweather = WeatherAPI().get_currently() icon = Image.open(os.path.join(config.icons_dir, 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["temp_c"]), curweather["condition"]["text"]), fill="black", anchor="ld", font=fnt_Big ) thisdayweather = list(itertools.islice(WeatherAPI().get_daily(), 1))[0] infos = { "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") image.paste(txt, (int(self.middle_align*width),int(height/2.6)), txt) # day fnt_Rig = ImageFont.truetype(config.fnt_R_path, 20) dayweather = list(itertools.islice(WeatherAPI().get_hourly(), 2)) 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), thisdayweather["condition"]["text"] + " aujourd'hui", fill="black", anchor="lm", font=fnt_Rig, maxwidth=(1-self.middle_align)*width) return image class WeatherMoonPhaseModule: def draw_module(self, config, width, height): image = Image.new('RGBA', (width, height), '#fff0') icon = Image.open(os.path.join(config.icons_dir, WeatherAPI().get_moon_icon())).resize((height, height)) image.paste(icon, (0,0), icon) return image class WeatherSunModule: def draw_module(self, config, width, height, start_align=0): image = Image.new('RGB', (width, height), '#fff') draw = ImageDraw.Draw(image) fnt_R = ImageFont.truetype(config.fnt_R_path, int(height*0.7)) thisdayweather = list(itertools.islice(WeatherAPI().get_daily(), 1))[0] import time infos = { "sunrise": thisdayweather["sunrise"], "sunset": thisdayweather["sunset"], } align = start_align for icon, info in infos.items(): if info == "": continue icon = Image.open(os.path.join(config.icons_dir, "wi-" + icon + ".png")).resize((height, height)) image.paste(icon, (align,0), icon) align += height + 2 draw.text( (align, height / 2), info, fill="black", anchor="lm", font=fnt_R ) align += fnt_R.getsize(info)[0] align += 10 return image class WeatherRainModule: def draw_module(self, config, width, height): if datetime.now().hour >= 21: thisdayweather = list(itertools.islice(WeatherAPI().get_daily(), 2))[1] else: 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["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}]) if thisdayweather["gust_kph"] > 45: icon_path = "wi-wind-beaufort-6.png" else: icon_path = "wi-umbrella.png" elif thisdayweather["uv"] > 4: gauge.add('Index UV', [{'value': thisdayweather["uv"] * 10}]) icon_path = "wi-hot.png" elif thisdayweather["maxwind_kph"] > 40: gauge.add('Vent', [{'value': thisdayweather["maxwind_kph"], 'color': '#999'}]) icon_path = "wi-strong-wind.png" elif thisdayweather["avgvis_km"] < 10: gauge.add('Visibilité', [{'value': thisdayweather["avgvis_km"] * 10, 'color': '#999'}]) icon_path = "wi-fog.png" elif "air_quality" in thisdayweather: 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" else: gauge.add('Vent', [{'value': thisdayweather["maxwind_kph"], 'color': '#999'}]) icon_path = "wi-wind-beaufort-1.png" image = Image.open(io.BytesIO(gauge.render_to_png())) if icon_path: icon = Image.open(os.path.join(config.icons_dir, icon_path)).resize((int(height/1.25), int(height/1.25))) image.paste(icon, (int(width/2-height/2.5), int(height-height/1.25)), icon) return image class WeatherTemperatureModule: def __init__(self): self.limit_futur = 30 def draw_module(self, config, width, height): 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 = [h for h in WeatherAPI().get_hourly()] hourly_min = 0 hourly_max = 0 for h in hours_weather: 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='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_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["temp_c"] for d in hours_weather[:self.limit_futur]], show_dots=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["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) # Add EcoWatt signal #for ecowatt #draw.rectangle( # (15 + day_size + int((day["temperatureLow"]-t_min)*t_scale),i*day_size + 4,day_size + int((day["temperatureHigh"]-t_min)*t_scale),i*day_size + 14), # fill="black") return img class WeeklyWeatherModule: def __init__(self): self.first_day = 1 self.limit_futur = 7 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, 14) fnt_B = ImageFont.truetype(config.fnt_RB_path, 14) 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 "", 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["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(os.path.join(config.icons_dir, 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["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(day["date"].strftime("%a") + " : " + day["condition"]["text"])[0] draw.text( (day_size + (width - day_size - summary_size) / 2, (i + 1) * day_size - 6), 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["condition"]["text"], fill="black", anchor="rs", font=fnt_R ) draw.text( (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["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["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 return image class WeatherAlerts: def gen_alerts(self): alerts = [] 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["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["headline"], "subtitle": subtitle, "description": alert["desc"], }) 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