weather api working -> 2025-01-27 17:49:29
4
.gitignore
vendored
|
@ -1,3 +1,7 @@
|
||||||
|
# Specific for my project
|
||||||
|
.cache.sqlite
|
||||||
|
|
||||||
|
# Python generic .gitignore
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 3.9 KiB |
BIN
assets/weather-icons/placeholder32.aseprite
Normal file
BIN
assets/weather-icons/placeholder32.bmp
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
assets/weather-icons/sunny-icon32.aseprite
Normal file
BIN
assets/weather-icons/sunny-icon32.bmp
Normal file
After Width: | Height: | Size: 2.1 KiB |
|
@ -1,9 +1,16 @@
|
||||||
{
|
{
|
||||||
"isDisplayConected": false,
|
"isDisplayConected": false,
|
||||||
|
|
||||||
"screenWidth": 250,
|
"screenWidth": 250,
|
||||||
"screenHeight": 122,
|
"screenHeight": 122,
|
||||||
"imageWidth": 250,
|
"imageWidth": 250,
|
||||||
"imageHeight": 122,
|
"imageHeight": 122,
|
||||||
"rotateAngle": 180,
|
"rotateAngle": 180,
|
||||||
"hourFormat": "12h"
|
|
||||||
|
"hourFormat": "12h",
|
||||||
|
|
||||||
|
"latitude": 40.4084,
|
||||||
|
"longitude": -3.6876,
|
||||||
|
"timezone": "Europe/Madrid",
|
||||||
|
"locales": "es_ES.UTF-8"
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,12 +11,15 @@ CSS stylesheets in the "render" folder.
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import locale
|
||||||
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from PIL import Image,ImageDraw,ImageFont
|
from PIL import Image,ImageDraw,ImageFont
|
||||||
|
|
||||||
|
from weather.weather import WeatherHelper
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Basic configuration settings (user replaceable)
|
# Basic configuration settings (user replaceable)
|
||||||
|
@ -26,45 +29,90 @@ def main():
|
||||||
|
|
||||||
|
|
||||||
isDisplayConected = config['isDisplayConected'] # set to true when debugging rendering without displaying to screen
|
isDisplayConected = config['isDisplayConected'] # set to true when debugging rendering without displaying to screen
|
||||||
screenWidth = config['screenWidth'] # Width of E-Ink display. Default is landscape. Need to rotate image to fit.
|
screenWidth = config['screenWidth'] # Width of E-Ink display. Default is landscape. Need to rotate image to fit.
|
||||||
screenHeight = config['screenHeight'] # Height of E-Ink display. Default is landscape. Need to rotate image to fit.
|
screenHeight = config['screenHeight'] # Height of E-Ink display. Default is landscape. Need to rotate image to fit.
|
||||||
imageWidth = config['imageWidth'] # Width of image to be generated for display.
|
imageWidth = config['imageWidth'] # Width of image to be generated for display.
|
||||||
imageHeight = config['imageHeight'] # Height of image to be generated for display.
|
imageHeight = config['imageHeight'] # Height of image to be generated for display.
|
||||||
rotateAngle = config['rotateAngle'] # If image is rendered in portrait orientation, angle to rotate to fit screen
|
rotateAngle = config['rotateAngle'] # If image is rendered in portrait orientation, angle to rotate to fit screen
|
||||||
hourFormat = config['hourFormat'] # The format the hour will be displayed. eg. 13:02 or 01:02 PM
|
hourFormat = config['hourFormat'] # The format the hour will be displayed. eg. 13:02 or 01:02 PM
|
||||||
|
latitude = config['latitude'] # A float. The latitude for the Weather API.
|
||||||
|
longitude = config['longitude'] # A float. The longitude for the Weather API.
|
||||||
|
timezone = config['timezone'] # The timezone is necesary for the Weather API
|
||||||
|
locales = config['locales'] # Set the locales for the month name
|
||||||
|
|
||||||
|
weatherService = WeatherHelper(latitude, longitude, timezone)
|
||||||
|
weather_data = weatherService.fetch_open_meteo_data()
|
||||||
|
|
||||||
|
locale.setlocale(locale.LC_TIME, locales)
|
||||||
|
|
||||||
# Set the hour, this is important to see what time the e-Paper has been syncronized
|
# Set the hour, this is important to see what time the e-Paper has been syncronized
|
||||||
if hourFormat == "12h":
|
if hourFormat == "12h":
|
||||||
time = dt.datetime.now().strftime("%I:%M %p")
|
time = dt.datetime.now().strftime("%I:%M %p")
|
||||||
else:
|
else:
|
||||||
time = dt.datetime.now().strftime("%H:%M")
|
time = dt.datetime.now().strftime("%H:%M")
|
||||||
|
|
||||||
|
day = dt.datetime.now().strftime("%A, %d %b")
|
||||||
|
|
||||||
|
|
||||||
assets = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'assets')
|
assets = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'assets')
|
||||||
|
weather_icons = os.path.join(assets, 'weather-icons')
|
||||||
fonts = os.path.join(assets, 'fonts')
|
fonts = os.path.join(assets, 'fonts')
|
||||||
# Drawing on the image
|
# Drawing on the image
|
||||||
font15 = ImageFont.truetype(os.path.join(fonts, 'wavesharefont.ttc'), 15)
|
|
||||||
font24 = ImageFont.truetype(os.path.join(fonts, 'wavesharefont.ttc'), 24)
|
|
||||||
font8 = ImageFont.truetype(os.path.join(os.path.join(fonts, 'pixel_operator'), 'PixelOperator8.ttf'), 8)
|
font8 = ImageFont.truetype(os.path.join(os.path.join(fonts, 'pixel_operator'), 'PixelOperator8.ttf'), 8)
|
||||||
font16 = ImageFont.truetype(os.path.join(os.path.join(fonts, 'pixel_operator'), 'PixelOperator.ttf'), 16)
|
font16 = ImageFont.truetype(os.path.join(os.path.join(fonts, 'pixel_operator'), 'PixelOperator.ttf'), 16)
|
||||||
font16_bold = ImageFont.truetype(os.path.join(os.path.join(fonts, 'pixel_operator'), 'PixelOperator-Bold.ttf'), 16)
|
font16_bold = ImageFont.truetype(os.path.join(os.path.join(fonts, 'pixel_operator'), 'PixelOperator-Bold.ttf'), 16)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#image = Image.open('assets/test4.bmp')
|
|
||||||
image = Image.new('1', (imageWidth, imageHeight), 255) # 255: clear the frame
|
image = Image.new('1', (imageWidth, imageHeight), 255) # 255: clear the frame
|
||||||
draw = ImageDraw.Draw(image)
|
draw = ImageDraw.Draw(image)
|
||||||
|
|
||||||
draw.text((0, 0), time, font = font16_bold, fill = 0) # draw the current time in the top left corner
|
draw.text((0, 0), f"{day}. {time}", font = font16_bold, fill = 0) # draw the current time in the top left corner
|
||||||
|
|
||||||
draw.line([(0,16),(250,16)], fill = 0,width = 2)
|
# Frames
|
||||||
draw.line([(125,16),(125,122)], fill = 0,width = 2)
|
draw.line([(0,16),(250,16)], fill = 0,width = 2) # Draw a line below the date
|
||||||
|
draw.line([(90,17),(90,122)], fill = 0,width = 2) # Line dividing the screeen in two parts
|
||||||
|
|
||||||
cal_icon = Image.open(os.path.join(assets, "cal-icon3.bmp"))
|
|
||||||
|
|
||||||
image.paste(cal_icon, (129,20))
|
# Calendar icon
|
||||||
|
cal_coordinates_x = 207
|
||||||
|
cal_coordinates_y = 2
|
||||||
|
|
||||||
draw.text((160, 26), 'Agenda', font = font16_bold, fill = 0)
|
cal_icon = Image.open(os.path.join(assets, "cal-icon3.bmp")) # calendar icon
|
||||||
|
# This seems complicates, but it just draw a white rectangle bellow the calendar
|
||||||
|
draw.rectangle([(cal_coordinates_x-2, cal_coordinates_y),(cal_coordinates_x + 28, cal_coordinates_y + 30)], fill = 255)
|
||||||
|
image.paste(cal_icon, (cal_coordinates_x, cal_coordinates_y))
|
||||||
|
|
||||||
|
# LEFT SEGMENT (WEATHER)
|
||||||
|
# Todays information
|
||||||
|
today_icon = weatherService.iconize_weather(weather_data["current_weather_code"])
|
||||||
|
today_icon = Image.open(os.path.join(weather_icons, today_icon))
|
||||||
|
image.paste(today_icon, (29,20))
|
||||||
|
|
||||||
|
today_temperature = f"{str(weather_data['current_temperature_2m'])}°"
|
||||||
|
draw.text((66,28), today_temperature, font = font16_bold, fill = 0) # The tomorrows temperature is bellow the icon
|
||||||
|
|
||||||
|
|
||||||
|
#draw.text((160, 26), 'Agenda', font = font16_bold, fill = 0)
|
||||||
|
|
||||||
|
# All the tomorrows information
|
||||||
|
tomorrow_icon = weatherService.iconize_weather(weather_data["tomorrow_weather_code"])
|
||||||
|
tomorrow_icon = Image.open(os.path.join(weather_icons, tomorrow_icon))
|
||||||
|
image.paste(tomorrow_icon, (6,74)) # The icon is in the bottom left corner
|
||||||
|
|
||||||
|
tomorrow_temperature = f"{str(weather_data['tomorrow_temperature_2m_max'])}/{str(weather_data['tomorrow_temperature_2m_min'])}°"
|
||||||
|
draw.text((6,106), tomorrow_temperature, font = font16_bold, fill = 0) # The tomorrows temperature is bellow the icon
|
||||||
|
|
||||||
|
# All the day after tomorrows information
|
||||||
|
day_after_icon = weatherService.iconize_weather(weather_data["day_after_weather_code"])
|
||||||
|
day_after_icon = Image.open(os.path.join(weather_icons, day_after_icon))
|
||||||
|
image.paste(day_after_icon, (52,74))
|
||||||
|
|
||||||
|
day_after_temperature = f"{str(weather_data['day_after_temperature_2m_max'])}/{str(weather_data['day_after_temperature_2m_min'])}°"
|
||||||
|
draw.text((52,106), day_after_temperature, font = font16_bold, fill = 0)
|
||||||
|
|
||||||
|
# RIGHT SEGMENT (CALENDAR/AGENDA)
|
||||||
|
# TODO: writing this part and also the logic part
|
||||||
|
|
||||||
# draw.line([(0,50),(50,0)], fill = 0,width = 1)
|
# draw.line([(0,50),(50,0)], fill = 0,width = 1)
|
||||||
# draw.chord((10, 60, 50, 100), 0, 360, fill = 0)
|
# draw.chord((10, 60, 50, 100), 0, 360, fill = 0)
|
||||||
|
@ -78,7 +126,6 @@ def main():
|
||||||
image.save(os.path.join(assets, 'output.bmp'))
|
image.save(os.path.join(assets, 'output.bmp'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if isDisplayConected:
|
if isDisplayConected:
|
||||||
from display.display import DisplayHelper
|
from display.display import DisplayHelper
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
import openmeteo_requests
|
||||||
|
|
||||||
|
import requests_cache
|
||||||
|
import pandas as pd
|
||||||
|
from retry_requests import retry
|
||||||
|
|
||||||
|
# Setup the Open-Meteo API client with cache and retry on error
|
||||||
|
cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)
|
||||||
|
retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
|
||||||
|
openmeteo = openmeteo_requests.Client(session = retry_session)
|
||||||
|
|
||||||
|
# Make sure all required weather variables are listed here
|
||||||
|
# The order of variables in hourly or daily is important to assign them correctly below
|
||||||
|
url = "https://api.open-meteo.com/v1/forecast"
|
||||||
|
params = {
|
||||||
|
"latitude": 40.408,
|
||||||
|
"longitude": -3.688,
|
||||||
|
"current": ["temperature_2m", "is_day", "weather_code"],
|
||||||
|
"daily": ["weather_code", "temperature_2m_max", "temperature_2m_min"],
|
||||||
|
"timezone": "Europe/Madrid",
|
||||||
|
"forecast_days": 3
|
||||||
|
}
|
||||||
|
responses = openmeteo.weather_api(url, params=params)
|
||||||
|
|
||||||
|
# Process first location. Add a for-loop for multiple locations or weather models
|
||||||
|
response = responses[0]
|
||||||
|
print(f"Coordinates {response.Latitude()}°N {response.Longitude()}°E")
|
||||||
|
print(f"Elevation {response.Elevation()} m asl")
|
||||||
|
print(f"Timezone {response.Timezone()} {response.TimezoneAbbreviation()}")
|
||||||
|
print(f"Timezone difference to GMT+0 {response.UtcOffsetSeconds()} s")
|
||||||
|
|
||||||
|
|
||||||
|
# Current values. The order of variables needs to be the same as requested.
|
||||||
|
current = response.Current()
|
||||||
|
|
||||||
|
current_temperature_2m = current.Variables(0).Value()
|
||||||
|
|
||||||
|
current_is_day = current.Variables(1).Value()
|
||||||
|
|
||||||
|
current_weather_code = current.Variables(2).Value()
|
||||||
|
|
||||||
|
print(f"Current time {current.Time()}")
|
||||||
|
|
||||||
|
print(f"Current temperature_2m {current_temperature_2m}")
|
||||||
|
print(f"Current is_day {current_is_day}")
|
||||||
|
print(f"Current weather_code {current_weather_code}")
|
||||||
|
# Process daily data. The order of variables needs to be the same as requested.
|
||||||
|
daily = response.Daily()
|
||||||
|
daily_weather_code = daily.Variables(0).ValuesAsNumpy()
|
||||||
|
daily_temperature_2m_max = daily.Variables(1).ValuesAsNumpy()
|
||||||
|
daily_temperature_2m_min = daily.Variables(2).ValuesAsNumpy()
|
||||||
|
|
||||||
|
daily_data = {"date": pd.date_range(
|
||||||
|
start = pd.to_datetime(daily.Time(), unit = "s", utc = True),
|
||||||
|
end = pd.to_datetime(daily.TimeEnd(), unit = "s", utc = True),
|
||||||
|
freq = pd.Timedelta(seconds = daily.Interval()),
|
||||||
|
inclusive = "left"
|
||||||
|
)}
|
||||||
|
|
||||||
|
daily_data["weather_code"] = daily_weather_code
|
||||||
|
daily_data["temperature_2m_max"] = daily_temperature_2m_max
|
||||||
|
daily_data["temperature_2m_min"] = daily_temperature_2m_min
|
||||||
|
|
||||||
|
daily_dataframe = pd.DataFrame(data = daily_data)
|
||||||
|
print(daily_dataframe)
|
|
@ -0,0 +1,148 @@
|
||||||
|
#!/usr/bin python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This part of the code exposes functions to fetch data from the open-meteo.com API.
|
||||||
|
The bellow code is the generated by the https://open-meteo.com/en/docs itself.
|
||||||
|
It has been slighly modified to return a dictionary with all the information nedeed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import openmeteo_requests
|
||||||
|
|
||||||
|
import requests_cache
|
||||||
|
#import pandas as pd
|
||||||
|
from retry_requests import retry
|
||||||
|
|
||||||
|
|
||||||
|
class WeatherHelper:
|
||||||
|
|
||||||
|
def __init__(self, latitude: float, longitude: float, timezone: str):
|
||||||
|
self.latitude = latitude
|
||||||
|
self.longitude = longitude
|
||||||
|
self.timezone = timezone
|
||||||
|
|
||||||
|
def fetch_open_meteo_data(self) -> dict:
|
||||||
|
|
||||||
|
# Setup the Open-Meteo API client with cache and retry on error
|
||||||
|
cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)
|
||||||
|
retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
|
||||||
|
openmeteo = openmeteo_requests.Client(session = retry_session)
|
||||||
|
|
||||||
|
# Make sure all required weather variables are listed here
|
||||||
|
# The order of variables in hourly or daily is important to assign them correctly below
|
||||||
|
url = "https://api.open-meteo.com/v1/forecast"
|
||||||
|
params = {
|
||||||
|
"latitude": self.latitude,
|
||||||
|
"longitude": self.longitude,
|
||||||
|
"current": ["temperature_2m", "is_day", "weather_code"],
|
||||||
|
"daily": ["weather_code", "temperature_2m_max", "temperature_2m_min"],
|
||||||
|
"timezone": self.timezone,
|
||||||
|
"forecast_days": 3
|
||||||
|
}
|
||||||
|
responses = openmeteo.weather_api(url, params=params)
|
||||||
|
|
||||||
|
# Process first location. Add a for-loop for multiple locations or weather models
|
||||||
|
response = responses[0]
|
||||||
|
#print(f"Coordinates {response.Latitude()}°N {response.Longitude()}°E")
|
||||||
|
#print(f"Elevation {response.Elevation()} m asl")
|
||||||
|
#print(f"Timezone {response.Timezone()} {response.TimezoneAbbreviation()}")
|
||||||
|
#print(f"Timezone difference to GMT+0 {response.UtcOffsetSeconds()} s")
|
||||||
|
|
||||||
|
|
||||||
|
# Current values. The order of variables needs to be the same as requested.
|
||||||
|
current = response.Current()
|
||||||
|
|
||||||
|
current_temperature_2m = current.Variables(0).Value()
|
||||||
|
|
||||||
|
current_is_day = current.Variables(1).Value()
|
||||||
|
|
||||||
|
current_weather_code = current.Variables(2).Value()
|
||||||
|
|
||||||
|
#print(f"Current time {current.Time()}")
|
||||||
|
|
||||||
|
#print(f"Current temperature_2m {current_temperature_2m}")
|
||||||
|
#print(f"Current is_day {current_is_day}")
|
||||||
|
#print(f"Current weather_code {current_weather_code}")
|
||||||
|
# Process daily data. The order of variables needs to be the same as requested.
|
||||||
|
daily = response.Daily()
|
||||||
|
daily_weather_code = daily.Variables(0).ValuesAsNumpy()
|
||||||
|
daily_temperature_2m_max = daily.Variables(1).ValuesAsNumpy()
|
||||||
|
daily_temperature_2m_min = daily.Variables(2).ValuesAsNumpy()
|
||||||
|
|
||||||
|
#daily_data = {"date": pd.date_range(
|
||||||
|
# start = pd.to_datetime(daily.Time(), unit = "s", utc = True),
|
||||||
|
# end = pd.to_datetime(daily.TimeEnd(), unit = "s", utc = True),
|
||||||
|
# freq = pd.Timedelta(seconds = daily.Interval()),
|
||||||
|
# inclusive = "left"
|
||||||
|
#)}
|
||||||
|
|
||||||
|
#daily_data["weather_code"] = daily_weather_code
|
||||||
|
#daily_data["temperature_2m_max"] = daily_temperature_2m_max
|
||||||
|
#daily_data["temperature_2m_min"] = daily_temperature_2m_min
|
||||||
|
|
||||||
|
#daily_dataframe = pd.DataFrame(data = daily_data)
|
||||||
|
#print(daily_dataframe)
|
||||||
|
|
||||||
|
# Here we are creating the dictionary that will be returned
|
||||||
|
weather_data = {}
|
||||||
|
|
||||||
|
weather_data['current_temperature_2m'] = int(current_temperature_2m)
|
||||||
|
weather_data['current_is_day'] = int(current_is_day)
|
||||||
|
weather_data['current_weather_code'] = int(current_weather_code)
|
||||||
|
|
||||||
|
weather_data['tomorrow_temperature_2m_max'] = int(daily_temperature_2m_max[1])
|
||||||
|
weather_data['tomorrow_temperature_2m_min'] = int(daily_temperature_2m_min[1])
|
||||||
|
weather_data['tomorrow_weather_code'] = int(daily_weather_code[1])
|
||||||
|
|
||||||
|
weather_data['day_after_temperature_2m_max'] = int(daily_temperature_2m_max[2])
|
||||||
|
weather_data['day_after_temperature_2m_min'] = int(daily_temperature_2m_min[2])
|
||||||
|
weather_data['day_after_weather_code'] = int(daily_weather_code[2])
|
||||||
|
|
||||||
|
return weather_data
|
||||||
|
|
||||||
|
|
||||||
|
def iconize_weather(self, weather_code: int) -> str:
|
||||||
|
|
||||||
|
# This are the WMO Weather interpretation codes (WW)
|
||||||
|
# More info in: https://open-meteo.com/en/docs at the end of the page.
|
||||||
|
# Here we only put the name of the icons, the path will be handle in main()
|
||||||
|
|
||||||
|
if weather_code >= 100:
|
||||||
|
weather_icon = "placeholder32.bmp"
|
||||||
|
return weather_icon
|
||||||
|
|
||||||
|
if weather_code in [0]:
|
||||||
|
weather_icon = "sunny-icon32.bmp"
|
||||||
|
elif weather_code in [1, 2, 3]:
|
||||||
|
weather_icon = "None"
|
||||||
|
elif weather_code in [45, 48]:
|
||||||
|
weather_icon = "None"
|
||||||
|
elif weather_code in [51, 53, 55]:
|
||||||
|
weather_icon = "None"
|
||||||
|
elif weather_code in [56, 57]:
|
||||||
|
weather_icon = "None"
|
||||||
|
elif weather_code in [61, 63, 65]:
|
||||||
|
weather_icon = "None"
|
||||||
|
elif weather_code in [66, 67]:
|
||||||
|
weather_icon = "None"
|
||||||
|
elif weather_code in [71, 73, 75]:
|
||||||
|
weather_icon = "None"
|
||||||
|
elif weather_code in [77]:
|
||||||
|
weather_icon = "None"
|
||||||
|
elif weather_code in [80, 81, 82]:
|
||||||
|
weather_icon = "None"
|
||||||
|
elif weather_code in [85, 86]:
|
||||||
|
weather_icon = "None"
|
||||||
|
elif weather_code in [95]:
|
||||||
|
weather_icon = "None"
|
||||||
|
elif weather_code in [96, 99]:
|
||||||
|
weather_icon = "None"
|
||||||
|
else:
|
||||||
|
weather_icon = "None"
|
||||||
|
|
||||||
|
# If there is no an icon for that code, search the next highest icon
|
||||||
|
if weather_icon == "None":
|
||||||
|
weather_icon = self.iconize_weather(weather_code + 1)
|
||||||
|
|
||||||
|
return weather_icon
|
||||||
|
|
||||||
|
|