424 lines
17 KiB
Python
424 lines
17 KiB
Python
from datetime import datetime, timezone, timedelta
|
|
import io
|
|
import itertools
|
|
import math
|
|
|
|
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("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["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("icons/" + 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("icons/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}])
|
|
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"] > 50:
|
|
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"
|
|
else:
|
|
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()))
|
|
|
|
if icon_path:
|
|
icon = Image.open("icons/" + 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("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["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
|