diff --git a/.gitignore b/.gitignore index 1800114..4221e6b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Specific for my project +.cache.sqlite + +# Python generic .gitignore # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/assets/output.bmp b/assets/output.bmp index 8bde196..18d689d 100644 Binary files a/assets/output.bmp and b/assets/output.bmp differ diff --git a/assets/test-characters-2bold.bmp b/assets/test-characters-2bold.bmp deleted file mode 100644 index 56a9011..0000000 Binary files a/assets/test-characters-2bold.bmp and /dev/null differ diff --git a/assets/test-characters-bold.bmp b/assets/test-characters-bold.bmp deleted file mode 100644 index 27288d8..0000000 Binary files a/assets/test-characters-bold.bmp and /dev/null differ diff --git a/assets/test-characters.bmp b/assets/test-characters.bmp deleted file mode 100644 index a806ace..0000000 Binary files a/assets/test-characters.bmp and /dev/null differ diff --git a/assets/test-fonts.bmp b/assets/test-fonts.bmp deleted file mode 100644 index 345f567..0000000 Binary files a/assets/test-fonts.bmp and /dev/null differ diff --git a/assets/weather-icons/placeholder32.aseprite b/assets/weather-icons/placeholder32.aseprite new file mode 100644 index 0000000..8eb1fc2 Binary files /dev/null and b/assets/weather-icons/placeholder32.aseprite differ diff --git a/assets/weather-icons/placeholder32.bmp b/assets/weather-icons/placeholder32.bmp new file mode 100644 index 0000000..996beed Binary files /dev/null and b/assets/weather-icons/placeholder32.bmp differ diff --git a/assets/weather-icons/sunny-icon32.aseprite b/assets/weather-icons/sunny-icon32.aseprite new file mode 100644 index 0000000..8eb1fc2 Binary files /dev/null and b/assets/weather-icons/sunny-icon32.aseprite differ diff --git a/assets/weather-icons/sunny-icon32.bmp b/assets/weather-icons/sunny-icon32.bmp new file mode 100644 index 0000000..996beed Binary files /dev/null and b/assets/weather-icons/sunny-icon32.bmp differ diff --git a/config.json b/config.json index 145f58b..1df5663 100644 --- a/config.json +++ b/config.json @@ -1,9 +1,16 @@ { "isDisplayConected": false, + "screenWidth": 250, "screenHeight": 122, "imageWidth": 250, "imageHeight": 122, "rotateAngle": 180, - "hourFormat": "12h" + + "hourFormat": "12h", + + "latitude": 40.4084, + "longitude": -3.6876, + "timezone": "Europe/Madrid", + "locales": "es_ES.UTF-8" } diff --git a/raspagenda.py b/raspagenda.py index f275424..8490131 100644 --- a/raspagenda.py +++ b/raspagenda.py @@ -11,12 +11,15 @@ CSS stylesheets in the "render" folder. import datetime as dt import os import sys +import locale import json import logging from PIL import Image,ImageDraw,ImageFont +from weather.weather import WeatherHelper + def main(): # Basic configuration settings (user replaceable) @@ -26,45 +29,90 @@ def main(): 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. - 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. - 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 - hourFormat = config['hourFormat'] # The format the hour will be displayed. eg. 13:02 or 01:02 PM + 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. + imageWidth = config['imageWidth'] # Width 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 + 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 if hourFormat == "12h": time = dt.datetime.now().strftime("%I:%M %p") else: 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') + weather_icons = os.path.join(assets, 'weather-icons') fonts = os.path.join(assets, 'fonts') # 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) 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) - #image = Image.open('assets/test4.bmp') image = Image.new('1', (imageWidth, imageHeight), 255) # 255: clear the frame 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) - draw.line([(125,16),(125,122)], fill = 0,width = 2) + # Frames + 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.chord((10, 60, 50, 100), 0, 360, fill = 0) @@ -78,7 +126,6 @@ def main(): image.save(os.path.join(assets, 'output.bmp')) - if isDisplayConected: from display.display import DisplayHelper diff --git a/test/test_weather.py b/test/test_weather.py index e69de29..91a1ce0 100644 --- a/test/test_weather.py +++ b/test/test_weather.py @@ -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) \ No newline at end of file diff --git a/weather/weather.py b/weather/weather.py index e69de29..984e0ad 100644 --- a/weather/weather.py +++ b/weather/weather.py @@ -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 + +