Compare commits

...

2 Commits

Author SHA1 Message Date
e8fd423e5f Add air quality indicators 2023-04-18 15:17:17 +02:00
7b0a3dd5ac Use WeatherAPI.com 2023-04-18 14:33:27 +02:00
3 changed files with 271 additions and 397 deletions

View File

@ -98,7 +98,11 @@ def main(only_on_coming_evt=False, ignore_module=[], force_coming_event=True, ex
if occuped_space < 250: if occuped_space < 250:
# weekly weather # weekly weather
from modules.weather import WeeklyWeatherModule 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 # RATP weather
major_lines = ["M7", "M14", "RB", "TT3A"] major_lines = ["M7", "M14", "RB", "TT3A"]

View File

@ -1,5 +1,6 @@
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
import io import io
import itertools
import math import math
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
@ -40,6 +41,44 @@ def draw_format_infos(infos, width, height, fnt_R, fnt_B, label_margin, align_he
return image 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: class WeatherToolbarModule:
def __init__(self): def __init__(self):
@ -55,10 +94,10 @@ class WeatherToolbarModule:
weather = WeatherAPI().get_currently() weather = WeatherAPI().get_currently()
infos = { infos = {
"Vent": "%d km/h" % weather["windSpeed"], "Vent": "%d km/h" % weather["wind_kph"],
"Humidité": "%d%%" % (weather["humidity"]), "Humidité": "%d%%" % (weather["humidity"]),
"Indice UV": str(weather["uvIndex"]), "Indice UV": str(int(weather["uv"])),
"Pression": "%d hPa" % weather["pressureSurfaceLevel"], "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") txt = draw_format_infos(infos, width, height, fnt_R, fnt_B, self.label_margin, align_height=height/2, anchor="lm")
@ -82,21 +121,21 @@ class WeatherJumboCurrentModule:
# current # current
curweather = WeatherAPI().get_currently() 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) image.paste(icon, (int(width*self.middle_align - height), 0), icon)
draw.text( draw.text(
(width*self.middle_align, height/3), (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 fill="black", anchor="ld", font=fnt_Big
) )
thisdayweather = WeatherAPI().get_daily()[0]["values"] thisdayweather = list(itertools.islice(WeatherAPI().get_daily(), 1))[0]
infos = { infos = {
"Ressentie": "%d˚" % curweather["temperatureApparent"], "Ressentie": "%d˚" % curweather["feelslike_c"],
"Minimale": "%d˚" % thisdayweather["temperatureMin"], "Minimale": "%d˚" % thisdayweather["mintemp_c"],
"Maximale": "%d˚" % thisdayweather["temperatureMax"], "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") 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 +144,12 @@ class WeatherJumboCurrentModule:
# day # day
fnt_Rig = ImageFont.truetype(config.fnt_R_path, 20) fnt_Rig = ImageFont.truetype(config.fnt_R_path, 20)
dailyweather = WeatherAPI().get_daily() dayweather = list(itertools.islice(WeatherAPI().get_hourly(), 2))
dayweather = WeatherAPI().get_hourly()
if dayweather[0]["values"]["weatherCode"] != dayweather[1]["values"]["weatherCode"]: if dayweather[0]["condition"]["text"] != dayweather[1]["condition"]["text"]:
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) 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: 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 return image
@ -133,12 +171,12 @@ class WeatherSunModule:
draw = ImageDraw.Draw(image) draw = ImageDraw.Draw(image)
fnt_R = ImageFont.truetype(config.fnt_R_path, int(height*0.7)) 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 import time
infos = { infos = {
"sunrise": WeatherAPI().read_timestamp(thisdayweather["sunriseTime"]).strftime("%X"), "sunrise": thisdayweather["sunrise"],
"sunset": WeatherAPI().read_timestamp(thisdayweather["sunsetTime"]).strftime("%X"), "sunset": thisdayweather["sunset"],
} }
align = start_align align = start_align
@ -163,32 +201,35 @@ class WeatherRainModule:
def draw_module(self, config, width, height): def draw_module(self, config, width, height):
if datetime.now().hour >= 21: if datetime.now().hour >= 21:
thisdayweather = WeatherAPI().get_daily()[1]["values"] thisdayweather = list(itertools.islice(WeatherAPI().get_daily(), 2))[1]
else: 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) 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) percent_formatter = lambda x: '{:.10g}%'.format(x)
gauge.value_formatter = percent_formatter gauge.value_formatter = percent_formatter
if thisdayweather["precipitationProbabilityMax"] == 0 and thisdayweather["uvIndexMax"] > 4: if thisdayweather["daily_chance_of_snow"] > 0:
gauge.add('Index UV', [{'value': thisdayweather["uvIndexMax"] * 10}]) gauge.add('Neige', [{'value': thisdayweather["daily_chance_of_snow"] + 1}])
icon_path = "wi-hot.png" icon_path = "wi-snowflake-cold.png"
elif thisdayweather["precipitationProbabilityMax"] > 0: elif thisdayweather["daily_chance_of_rain"] > 0:
gauge.add('Pluie', [{'value': thisdayweather["precipitationProbabilityMax"] + 1}]) gauge.add('Pluie', [{'value': thisdayweather["daily_chance_of_rain"] + 1}])
icon_path = "wi-umbrella.png" icon_path = "wi-umbrella.png"
elif thisdayweather["windSpeedMax"] > 50: elif thisdayweather["uv"] > 4:
gauge.add('Vent', [{'value': thisdayweather["windSpeedMax"], 'color': '#999'}]) 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" icon_path = "wi-strong-wind.png"
elif thisdayweather["visibilityMin"] < 10: elif thisdayweather["avgvis_km"] < 10:
gauge.add('Visibilité', [{'value': thisdayweather["visibilityMin"] * 10, 'color': '#999'}]) gauge.add('Visibilité', [{'value': thisdayweather["avgvis_km"] * 10, 'color': '#999'}])
icon_path = "wi-fog.png" icon_path = "wi-fog.png"
elif thisdayweather["cloudCoverAvg"] > 40:
gauge.add('Couverture nuageuse', [{'value': thisdayweather["cloudCoverAvg"], 'color': '#999'}])
icon_path = "wi-cloudy.png"
else: else:
gauge.add('Pluie', [{'value': thisdayweather["precipitationProbabilityMax"]}]) gauge.add("Qualité de l'air", [{'value': thisdayweather["air_quality"]["gb-defra-index"] * 10}])
icon_path = "wi-na.png" 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())) image = Image.open(io.BytesIO(gauge.render_to_png()))
@ -204,33 +245,33 @@ class WeatherTemperatureModule:
self.limit_futur = 30 self.limit_futur = 30
def draw_module(self, config, width, height): def draw_module(self, config, width, height):
thisdayweather = WeatherAPI().get_daily() thisdayweather = list(itertools.islice(WeatherAPI().get_daily(), 2))
if datetime.now().hour >= 19 and thisdayweather[1]["precipitationProbabilityMax"] > thisdayweather[0]["precipitationProbabilityMax"]: if datetime.now().hour >= 19 and thisdayweather[1]["totalprecip_mm"] > thisdayweather[0]["totalprecip_mm"]:
thisdayweather = thisdayweather[1] thisdayweather = thisdayweather[1]
else: else:
thisdayweather = thisdayweather[0] thisdayweather = thisdayweather[0]
hours_weather = WeatherAPI().get_hourly() hours_weather = [h for h in WeatherAPI().get_hourly()]
hourly_min = 0 hourly_min = 0
hourly_max = 0 hourly_max = 0
for h in hours_weather: for h in hours_weather:
if hourly_min > h["values"]["temperature"]: if hourly_min > h["temp_c"]:
hourly_min = h["values"]["temperature"] hourly_min = h["temp_c"]
if hourly_max < h["values"]["temperature"]: if hourly_max < h["temp_c"]:
hourly_max = h["values"]["temperature"] 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.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: if thisdayweather["totalprecip_mm"] + thisdayweather["totalsnow_cm"] > 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) 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: 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())) img = Image.open(io.BytesIO(line_chart.render_to_png()))
draw = ImageDraw.Draw(img) draw = ImageDraw.Draw(img)
@ -255,60 +296,60 @@ class WeeklyWeatherModule:
fnt_R = ImageFont.truetype(config.fnt_R_path, 14) fnt_R = ImageFont.truetype(config.fnt_R_path, 14)
fnt_B = ImageFont.truetype(config.fnt_RB_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]) nbdays = len(weekweather[self.first_day:self.first_day+self.limit_futur])
day_size = min(40, int(height / (nbdays + 1))) day_size = min(40, int(height / (nbdays + 1)))
display_longtext(draw, display_longtext(draw,
(day_size + (width - day_size)/2, day_size / 2 + 5), (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, fill="black", anchor="mm", font=fnt_B,
maxwidth=width - day_size maxwidth=width - day_size
) )
temp = [] temp = []
for day in weekweather[self.first_day:self.first_day+self.limit_futur]: for day in weekweather[self.first_day:self.first_day+self.limit_futur]:
temp.append(day["values"]["temperatureMin"]) temp.append(day["mintemp_c"])
temp.append(day["values"]["temperatureMax"]) temp.append(day["maxtemp_c"])
t_min = min(temp) t_min = min(temp)
t_max = max(temp) t_max = max(temp)
t_scale = (width - day_size - 30) / (t_max - t_min) t_scale = (width - day_size - 30) / (t_max - t_min)
i = 1 i = 1
for day in weekweather[self.first_day:self.first_day+self.limit_futur]: 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) image.paste(icon, (0, i * day_size), icon)
draw.text( draw.text(
(15 + 2 + day_size + int((day["values"]["temperatureMin"]-t_min)*t_scale), i*day_size + 4), (15 + 2 + day_size + int((day["mintemp_c"]-t_min)*t_scale), i*day_size + 4),
"%d˚" % math.trunc(day["values"]["temperatureMax"]), "%d˚" % math.trunc(day["mintemp_c"]),
fill="black", anchor="rt", font=fnt_R 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( draw.text(
(day_size + (width - day_size - summary_size) / 2, (i + 1) * day_size - 6), (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 fill="#666", anchor="ls", font=fnt_R
) )
draw.text( draw.text(
(day_size + (width - day_size + summary_size) / 2, (i + 1) * day_size - 6), (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 fill="black", anchor="rs", font=fnt_R
) )
draw.text( draw.text(
(day_size + int((day["values"]["temperatureMax"]-t_min)*t_scale) + 2, i * day_size + 4), (day_size + int((day["maxtemp_c"]-t_min)*t_scale) + 2, i * day_size + 4),
"%d˚" % math.trunc(day["values"]["temperatureMax"]), "%d˚" % math.trunc(day["maxtemp_c"]),
fill="black", anchor="lt", font=fnt_R fill="black", anchor="lt", font=fnt_R
) )
try: try:
draw.rounded_rectangle( 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") radius=5, fill="black")
except AttributeError: except AttributeError:
draw.rectangle( 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") fill="black")
i += 1 i += 1
@ -320,30 +361,63 @@ class WeatherAlerts:
def gen_alerts(self): def gen_alerts(self):
alerts = [] alerts = []
if WeatherAPI().has_alerts(): for alert in WeatherAPI().get_alerts():
for alert in WeatherAPI().get_alerts(): if alert["severity"] == "Moderate":
if alert["severity"] == "watch" or alert["title"].startswith("Moderate"): icon = "wi-small-craft-advisory.png"
icon = "wi-small-craft-advisory.png" elif alert["severity"] != "Moderate":
elif alert["severity"] == "warning": icon = "wi-gale-warning.png"
icon = "wi-gale-warning.png" else:
else: icon = None
icon = None
startTime = WeatherAPI().read_timestamp(alert["time"]) startTime = WeatherAPI().read_timestamp(alert["effective"])
endTime = WeatherAPI().read_timestamp(alert["expires"]) endTime = WeatherAPI().read_timestamp(alert["expires"])
# Show alert timing if under a day # Show alert timing if under a day
if startTime.hour != endTime.hour: 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") 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: elif startTime.day != datetime.now().day:
subtitle = startTime.strftime("%x") subtitle = startTime.strftime("%x")
else: else:
subtitle = "" subtitle = ""
alerts.append({ alerts.append({
"icon": icon, "icon": icon,
"title": alert["title"], "title": alert["headline"],
"subtitle": subtitle, "subtitle": subtitle,
"description": alert["description"], "description": alert["desc"],
}) })
return alerts 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

View File

@ -1,4 +1,5 @@
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import itertools
import json import json
import os import os
import urllib.error import urllib.error
@ -6,33 +7,41 @@ import urllib.parse
import urllib.request import urllib.request
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
class TomorrowAPI: class WeatherAPI:
def __init__(self, apikey=None, gps=None, opts={"units": "metric"}): def __init__(self, apikey=None, gps=None, opts={"lang": "fr", "units": "metric"}):
self.apikey = apikey or os.environ["TOMORROWAPIKEY"] self.apikey = apikey or os.environ["WEATHERAPIKEY"]
self.baseurl = "https://api.tomorrow.io/v4" 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.default_gps = gps or ("GPS" in os.environ and os.environ["GPS"]) or "48.8127,2.3437"
self.opts = opts self.opts = opts
self._cached_file = ".weather-%s-%s.cache" 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: if gps is None:
gps = self.default_gps gps = self.default_gps
cached_filename = self._cached_file % (apitype + ("" if "dt" not in params else "-{}".format(params["dt"])), gps)
# Read the mod time # Read the mod time
statinfo = None statinfo = None
try: try:
statinfo = os.stat(self._cached_file % (apitype, gps)) statinfo = os.stat(cached_filename)
except: except:
pass 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): 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 # Do the request and save it
try: 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 urllib.request.urlopen(self.baseurl + "/" + apitype + ".json?" + urllib.parse.urlencode(params)) as f:
with open(self._cached_file % (apitype, gps), 'wb') as fd: with open(cached_filename, 'wb') as fd:
fd.write(f.read()) fd.write(f.read())
except ConnectionResetError: except ConnectionResetError:
pass pass
@ -42,7 +51,7 @@ class TomorrowAPI:
# Retrieve cached data # Retrieve cached data
res = {} res = {}
with open(self._cached_file % (apitype, gps)) as f: with open(cached_filename) as f:
res = json.load(f) res = json.load(f)
return res return res
@ -54,366 +63,153 @@ class TomorrowAPI:
return "wi-day-sunny.png" return "wi-day-sunny.png"
else: else:
return "wi-night-clear.png" 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: if not night:
return "wi-day-sunny-overcast.png" return "wi-day-sunny-overcast.png"
else: else:
return "wi-night-alt-partly-cloudy.png" return "wi-night-alt-partly-cloudy.png"
elif icon == 1101: elif icon == 1030 or icon == 1072:
if not night: if not night:
return "wi-day-cloudy-high.png" return "wi-day-haze.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"
else: else:
return "wi-night-fog.png" return "wi-night-fog.png"
elif icon == 4000: elif icon == 1063:
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:
if not night: if not night:
return "wi-day-rain.png" return "wi-day-rain.png"
else: else:
return "wi-night-alt-rain.png" return "wi-night-alt-rain.png"
elif icon == 5001 or icon == 5100 or icon == 5000 or icon == 5101: elif icon == 1066:
return "wi-snow.png" if not night:
elif icon == 5115 or icon == 5116: return "wi-day-snow.png"
else:
return "wi-night-alt-snow.png"
elif icon == 1069 or icon == 1204:
if not night: if not night:
return "wi-day-sleet.png" return "wi-day-sleet.png"
else: else:
return "wi-night-sleet.png" return "wi-night-alt-sleet.png"
elif icon == 5122 or icon == 5110 or icon == 5108 or icon == 5114 or icon == 5112: 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" 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: if not night:
return "wi-day-snow.png" return "wi-day-snow.png"
else: else:
return "wi-night-snow.png" 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: elif icon == 1237:
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:
return "wi-hail.png" 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: if not night:
return "wi-day-hail.png" return "wi-day-hail.png"
else: else:
return "wi-night-hail.png" return "wi-night-hail.png"
elif icon == 8000: elif icon == 1273 or icon == 1276:
return "wi-thunderstorm.png"
elif icon == 8001 or icon == 8003 or icon == 8002:
if not night: if not night:
return "wi-day-thunderstorm.png" return "wi-day-thunderstorm.png"
else: else:
return "wi-night-thunderstorm.png" 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: else:
return "wi-alien.png" 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): 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" return "wi-moon-alt-new.png"
elif moon_phase < 0.071: elif moon_phase == "Waxing Crescent":
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:
return "wi-moon-alt-waxing-crescent-4.png" return "wi-moon-alt-waxing-crescent-4.png"
elif moon_phase < 0.214: elif moon_phase == "First Quarter":
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:
return "wi-moon-alt-first-quarter.png" return "wi-moon-alt-first-quarter.png"
elif moon_phase < 0.321: elif moon_phase == "Waxing Gibbous":
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:
return "wi-moon-alt-waxing-gibbous-4.png" return "wi-moon-alt-waxing-gibbous-4.png"
elif moon_phase < 0.464: elif moon_phase == "Full Moon":
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:
return "wi-moon-alt-full.png" return "wi-moon-alt-full.png"
elif moon_phase < 0.571: elif moon_phase == "Waning Gibbous":
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:
return "wi-moon-alt-waning-gibbous-4.png" return "wi-moon-alt-waning-gibbous-4.png"
elif moon_phase < 0.714: elif moon_phase == "Last Quarter":
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:
return "wi-moon-alt-third-quarter.png" 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: else:
return "wi-moon-alt-waning-crescent-6.png" return "wi-moon-alt-waning-crescent-3.png"
def get_currently(self, *args, **kwargs): 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): 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): def get_daily(self, *args, **kwargs):
return self.get_weather(*args, **kwargs)["timelines"]["daily"] for d in self.get_forecast(*args, **kwargs):
yield {
def has_alerts(self, *args, **kwargs): **d["day"],
return "alerts" in self.get_weather(*args, **kwargs) and len(self.get_weather(*args, **kwargs)["alerts"]) > 0 **d["astro"]
}
def get_alerts(self, *args, **kwargs): 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): def read_timestamp(self, timestamp, *args, **kwargs):
PARIS = ZoneInfo("Europe/Paris") 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__': if __name__ == '__main__':
dsa = TomorrowAPI() dsa = WeatherAPI()
print(dsa.get_currently()) print(json.dumps(dsa.get_currently()))
print(dsa.get_daily()) #print(json.dumps([d for d in dsa.get_daily()][0]))