663 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			663 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """
 | |
| Inkycal fullscreen weather module
 | |
| Copyright by mrbwburns
 | |
| """
 | |
| import io
 | |
| import locale
 | |
| import logging
 | |
| import math
 | |
| import os
 | |
| from datetime import datetime
 | |
| 
 | |
| import matplotlib.dates as mdates
 | |
| import matplotlib.pyplot as plt
 | |
| import matplotlib.ticker as ticker
 | |
| import numpy as np
 | |
| from dateutil import tz
 | |
| from PIL import Image
 | |
| from PIL import ImageDraw
 | |
| from PIL import ImageFont
 | |
| from PIL import ImageOps
 | |
| 
 | |
| from icons.weather_icons.weather_icons import get_weather_icon
 | |
| from inkycal.custom.functions import fonts
 | |
| from inkycal.custom.functions import get_system_tz
 | |
| from inkycal.custom.functions import internet_available
 | |
| from inkycal.custom.functions import top_level
 | |
| from inkycal.custom.inkycal_exceptions import NetworkNotReachableError
 | |
| from inkycal.custom.openweathermap_wrapper import OpenWeatherMap
 | |
| from inkycal.modules.inky_image import image_to_palette
 | |
| from inkycal.modules.template import inkycal_module
 | |
| 
 | |
| logger = logging.getLogger(__name__)
 | |
| logger.setLevel(logging.INFO)
 | |
| 
 | |
| icons_dir = os.path.join(top_level, "icons", "ui-icons")
 | |
| 
 | |
| 
 | |
| def outline(image: Image, size: int, color: tuple) -> Image:
 | |
|     # Create a canvas for the outline image
 | |
|     outlined = Image.new("RGBA", image.size, (0, 0, 0, 0))
 | |
| 
 | |
|     # Make a black outline
 | |
|     for x in range(image.width):
 | |
|         for y in range(image.height):
 | |
|             pixel = image.getpixel((x, y))
 | |
|             if pixel[0] != 0 or pixel[1] != 0 or pixel[2] != 0:
 | |
|                 outlined.putpixel((x, y), color)
 | |
| 
 | |
|     # Enlarge the outlined image, and paste the original image on top to create a shadow effect
 | |
|     outlined = outlined.resize((outlined.width + size, outlined.height + size))
 | |
|     paste_position = ((outlined.width - image.width) // 2, (outlined.height - image.height) // 2)
 | |
|     outlined.paste(image, paste_position, image)
 | |
| 
 | |
|     # Create a mask to prevent transparent pixels from overwriting
 | |
|     mask = Image.new("L", outlined.size, 255)
 | |
|     outlined = Image.composite(outlined, Image.new("RGBA", outlined.size, (0, 0, 0, 0)), mask)
 | |
| 
 | |
|     return outlined
 | |
| 
 | |
| 
 | |
| def get_image_from_plot(fig: plt) -> Image:
 | |
|     buf = io.BytesIO()
 | |
|     fig.savefig(buf)
 | |
|     buf.seek(0)
 | |
|     return Image.open(buf)
 | |
| 
 | |
| 
 | |
| class Fullweather(inkycal_module):
 | |
|     """Fullscreen Weather class
 | |
|     gets weather details from openweathermap and plots a nice fullscreen forecast picture
 | |
|     """
 | |
| 
 | |
|     name = "Fullscreen weather (openweathermap) - Get weather forecasts from openweathermap"
 | |
| 
 | |
|     requires = {
 | |
|         "api_key": {
 | |
|             "label": "Please enter openweathermap api-key. You can create one for free on openweathermap",
 | |
|         },
 | |
|         "latitude": {"label": "Please enter your location' geographical latitude. E.g. 51.51 for London."},
 | |
|         "longitude": {"label": "Please enter your location' geographical longitude. E.g. -0.13 for London."},
 | |
|     }
 | |
| 
 | |
|     optional = {
 | |
|         "api_version": {
 | |
|             "label": "Please enter openweathermap api version. Default is '2.5'.",
 | |
|             "options": ["2.5", "3.0"],
 | |
|         },
 | |
|         "orientation": {"label": "Please select the desired orientation", "options": ["vertical", "horizontal"]},
 | |
|         "temp_unit": {
 | |
|             "label": "Which temperature unit should be used?",
 | |
|             "options": ["celsius", "fahrenheit"],
 | |
|         },
 | |
|         "wind_unit": {
 | |
|             "label": "Which wind speed unit should be used?",
 | |
|             "options": ["beaufort", "knots", "miles_hour", "km_hour", "meters_sec"],
 | |
|         },
 | |
|         "wind_gusts": {
 | |
|             "label": "Should current wind gust speed also be displayed?",
 | |
|             "options": [True, False],
 | |
|         },
 | |
|         "keep_history": {
 | |
|             "label": "Should the weather data be written to local json files (one per query)?",
 | |
|             "options": [True, False],
 | |
|         },
 | |
|         "min_max_annotations": {
 | |
|             "label": "Should the temperature plot have min/max annotation labels?",
 | |
|             "options": [True, False],
 | |
|         },
 | |
|         "locale": {
 | |
|             "label": "Your locale",
 | |
|             "options": ["de_DE.UTF-8", "en_GB.UTF-8"],
 | |
|         },
 | |
|         "font": {
 | |
|             "label": "Font family to use for the entire screen",
 | |
|             "options": ["NotoSans", "Roboto", "Poppins"],
 | |
|         },
 | |
|         "chart_title": {
 | |
|             "label": "Title of the temperature and precipitation plot",
 | |
|             "options": ["Temperatur und Niederschlag", "Temperature and precipitation"],
 | |
|         },
 | |
|         "weekly_title": {
 | |
|             "label": "Title of the weekly weather forecast",
 | |
|             "options": ["Tageswerte", "Weekly forecast"],
 | |
|         },
 | |
|         "icon_outline": {
 | |
|             "label": "Should the weather icons have outlines?",
 | |
|             "options": [True, False],
 | |
|         },
 | |
|     }
 | |
| 
 | |
|     def __init__(self, config):
 | |
|         """Initialize inkycal_weather module"""
 | |
| 
 | |
|         super().__init__(config)
 | |
| 
 | |
|         config = config["config"]
 | |
| 
 | |
|         self.tz = get_system_tz()
 | |
| 
 | |
|         # Check if all required parameters are present
 | |
|         for param in self.requires:
 | |
|             if not param in config:
 | |
|                 raise Exception(f"config is missing {param}")
 | |
| 
 | |
|         # required parameters
 | |
|         self.api_key = config["api_key"]
 | |
|         self.location_lat = float(config["latitude"])
 | |
|         self.location_lon = float(config["longitude"])
 | |
|         self.font_size = int(config["fontsize"])
 | |
| 
 | |
|         # optional parameters
 | |
|         if "api_version" in config and config["api_version"] == "3.0":
 | |
|             self.owm_api_version = "3.0"
 | |
|         else:
 | |
|             self.owm_api_version = "2.5"
 | |
|         if "orientation" in config:
 | |
|             self.orientation = config["orientation"]
 | |
|             assert self.orientation in ["horizontal", "vertical"]
 | |
|         else:
 | |
|             self.orientation = "horizontal"
 | |
|         if "wind_unit" in config:
 | |
|             self.wind_unit = config["wind_unit"]
 | |
|         else:
 | |
|             self.wind_unit = "meters_sec"
 | |
|         if self.wind_unit == "beaufort":
 | |
|             self.windDispUnit = "bft"
 | |
|         elif self.wind_unit == "knots":
 | |
|             self.windDispUnit = "kn"
 | |
|         elif self.wind_unit == "km_hour":
 | |
|             self.windDispUnit = "km/h"
 | |
|         elif self.wind_unit == "miles_hour":
 | |
|             self.windDispUnit = "mph"
 | |
|         else:
 | |
|             self.windDispUnit = "m/s"
 | |
| 
 | |
|         if "wind_gusts" in config:
 | |
|             self.wind_gusts = bool(config["wind_gusts"])
 | |
|         else:
 | |
|             self.wind_gusts = True
 | |
| 
 | |
|         if "temp_unit" in config:
 | |
|             self.temp_unit = config["temp_unit"]
 | |
|         else:
 | |
|             self.temp_unit = "celsius"
 | |
|         if self.temp_unit == "fahrenheit":
 | |
|             self.tempDispUnit = "F"
 | |
|         elif self.temp_unit == "celsius":
 | |
|             self.tempDispUnit = "°"
 | |
| 
 | |
|         if "weekly_title" in config:
 | |
|             self.weekly_title = config["weekly_title"]
 | |
|         else:
 | |
|             self.weekly_title = "Weekly forecast"
 | |
| 
 | |
|         if "chart_title" in config:
 | |
|             self.chart_title = config["chart_title"]
 | |
|         else:
 | |
|             self.chart_title = "Temperature and precipitation"
 | |
| 
 | |
|         if "keep_history" in config:
 | |
|             self.keep_history = config["keep_history"]
 | |
|         else:
 | |
|             self.keep_history = False
 | |
| 
 | |
|         if "min_max_annotations" in config:
 | |
|             self.min_max_annotations = bool(config["min_max_annotations"])
 | |
|         else:
 | |
|             self.min_max_annotations = False
 | |
| 
 | |
|         if "locale" in config:
 | |
|             self.locale = config["locale"]
 | |
|         else:
 | |
|             self.locale = "en_GB.UTF-8"
 | |
|         locale.setlocale(locale.LC_TIME, self.locale)
 | |
|         self.language = self.locale.split("_")[0]
 | |
| 
 | |
|         if "icon_outline" in config:
 | |
|             self.icon_outline = config["icon_outline"]
 | |
|         else:
 | |
|             self.icon_outline = True
 | |
| 
 | |
|         if "font" in config:
 | |
|             self.font = config["font"]
 | |
|         else:
 | |
|             self.font = "Roboto"
 | |
| 
 | |
|         # some calculations for scalability
 | |
|         # TODO: make this work for all sizes
 | |
|         if self.orientation == "horizontal":
 | |
|             self.width, self.height = self.height, self.width
 | |
|         self.screen_width_in = 163 / 25.4  # 163 mm for 7in5
 | |
|         self.screen_height_in = 98 / 25.4  # 98 mm for 7in5
 | |
|         self.dpi = math.sqrt(
 | |
|             (float(self.width) ** 2 + float(self.height) ** 2)
 | |
|             / (self.screen_width_in**2 + self.screen_height_in**2)
 | |
|         )
 | |
|         self.left_section_width = int(self.width / 4)
 | |
| 
 | |
|         # give an OK message
 | |
|         print(f"{__name__} loaded")
 | |
| 
 | |
|     def createBaseImage(self):
 | |
|         """
 | |
|         Creates background and adds current date
 | |
|         """
 | |
|         # Create white image
 | |
|         self.image = Image.new("RGB", (self.width, self.height), (255, 255, 255))
 | |
|         image_draw = ImageDraw.Draw(self.image)
 | |
| 
 | |
|         # Create black rectangle for the current weather section
 | |
|         rect_width = int(self.width / 4)
 | |
|         image_draw.rectangle((0, 0, rect_width, self.height), fill=0)
 | |
| 
 | |
|         # Add text with current date
 | |
|         tz_info = tz.gettz(self.tz)
 | |
|         dateString = datetime.now(tz=tz_info).strftime("%d. %B")
 | |
|         dateFont = self.get_font(style="Bold", size=self.font_size)
 | |
|         # Get the width of the text
 | |
|         dateStringbbox = dateFont.getbbox(dateString)
 | |
|         dateW = dateStringbbox[2] - dateStringbbox[0]
 | |
|         # Draw the current date centered
 | |
|         image_draw.text(((rect_width - dateW) / 2, 5), dateString, font=dateFont, fill=(255, 255, 255))
 | |
| 
 | |
|     def addUserSection(self):
 | |
|         """
 | |
|         Adds user-defined section to the given image
 | |
|         """
 | |
|         ## Create drawing object for image
 | |
|         image_draw = ImageDraw.Draw(self.image)
 | |
| 
 | |
|         if False:  # self.mqtt_sub == True:
 | |
|             # Add icon for Home
 | |
|             homeTempIcon = Image.open(os.path.join(icons_dir, "home_temp.png"))
 | |
|             homeTempIcon = ImageOps.invert(homeTempIcon)
 | |
|             homeTempIcon = homeTempIcon.resize((40, 40))
 | |
|             homeTemp_y = int(self.height * 0.8125)
 | |
|             self.image.paste(homeTempIcon, (15, homeTemp_y))
 | |
| 
 | |
|             # Home temperature
 | |
|             # my_home = mqtt_temperature(host=mqtt_host, port=mqtt_port, user=mqtt_user, password=mqtt_pass, topic=mqtt_topic)
 | |
|             # homeTemp = None
 | |
|             # while homeTemp == None:
 | |
|             #    homeTemp = my_home.get_temperature()
 | |
|             # homeTempString = f"{homeTemp:.1f} {tempDispUnit}"
 | |
|             # homeTempFont = font.font(font_family, "Bold", 28)
 | |
|             # image_draw.text((65, homeTemp_y), homeTempString, font=homeTempFont, fill=(255, 255, 255))
 | |
| 
 | |
|             # Add icon for rH
 | |
|             humidityIcon = Image.open(os.path.join(icons_dir, "humidity.bmp"))
 | |
|             humidityIcon = humidityIcon.resize((40, 40))
 | |
|             humidity_y = int(self.height * 0.90625)
 | |
|             self.image.paste(humidityIcon, (15, humidity_y))
 | |
| 
 | |
|             # rel. humidity
 | |
|             # rH = None
 | |
|             # while rH == None:
 | |
|             #    rH = my_home.get_rH()
 | |
|             # humidityString = f"{rH:.0f} %"
 | |
|             # humidityFont = font.font(font_family, "Bold", 28)
 | |
|             # image_draw.text((65, humidity_y), humidityString, font=humidityFont, fill=(255, 255, 255))
 | |
|         else:
 | |
|             # Add icon for Humidity
 | |
|             humidityIcon = Image.open(os.path.join(icons_dir, "humidity.bmp"))
 | |
|             humidityIcon = humidityIcon.resize((40, 40))
 | |
|             humidity_y = int(self.height * 0.8125)
 | |
|             self.image.paste(humidityIcon, (15, humidity_y))
 | |
| 
 | |
|             # Humidity
 | |
|             humidityString = f"{self.current_weather['humidity']} %"
 | |
|             humidityFont = self.get_font("Bold", self.font_size + 8)
 | |
|             image_draw.text((65, humidity_y), humidityString, font=humidityFont, fill=(255, 255, 255))
 | |
| 
 | |
|             # Add icon for uv
 | |
|             uvIcon = Image.open(os.path.join(icons_dir, "uv.bmp"))
 | |
|             uvIcon = uvIcon.resize((40, 40))
 | |
|             ux_y = int(self.height * 0.90625)
 | |
|             self.image.paste(uvIcon, (15, ux_y))
 | |
| 
 | |
|             # uvindex
 | |
|             uvi = self.current_weather["uvi"] if self.current_weather["uvi"] else 0.0
 | |
|             uvString = f"{uvi:.1f}"
 | |
|             uvFont = self.get_font("Bold", self.font_size + 8)
 | |
|             image_draw.text((65, ux_y), uvString, font=uvFont, fill=(255, 255, 255))
 | |
| 
 | |
|     def addCurrentWeather(self):
 | |
|         """
 | |
|         Adds current weather situation to the left section of the image
 | |
|         """
 | |
|         ## Create drawing object for image
 | |
|         image_draw = ImageDraw.Draw(self.image)
 | |
| 
 | |
|         ## Add detailed weather status text to the image
 | |
|         sumString = self.current_weather["detailed_status"].replace(" ", "\n ")
 | |
|         sumFont = self.get_font("Regular", self.font_size + 8)
 | |
|         maxW = 0
 | |
|         totalH = 0
 | |
|         for word in sumString.split("\n "):
 | |
|             sumStringbbox = sumFont.getbbox(word)
 | |
|             sumW = sumStringbbox[2] - sumStringbbox[0]
 | |
|             sumH = sumStringbbox[3] - sumStringbbox[1]
 | |
|             maxW = max(maxW, sumW)
 | |
|             totalH += sumH
 | |
|         sumtext_x = int((self.left_section_width - maxW) / 2)
 | |
|         sumtext_y = int(self.height * 0.19) - totalH
 | |
|         image_draw.multiline_text((sumtext_x, sumtext_y), sumString, font=sumFont, fill=(255, 255, 255), align="center")
 | |
|         logger.debug(f"Added current weather detailed status text: {sumString} at x:{sumtext_x}/y:{sumtext_y}.")
 | |
| 
 | |
|         ## Add current weather icon to the image
 | |
|         icon = get_weather_icon(icon_name=self.current_weather["weather_icon_name"], size=150)
 | |
|         # Create a mask from the alpha channel of the weather icon
 | |
|         if len(icon.split()) == 4:
 | |
|             mask = icon.split()[-1]
 | |
|         else:
 | |
|             mask = None
 | |
|         # Paste the foreground of the icon onto the background with the help of the mask
 | |
|         icon_x = int((self.left_section_width - icon.width) / 2)
 | |
|         icon_y = int(self.height * 0.2)
 | |
|         self.image.paste(icon, (icon_x, icon_y), mask)
 | |
| 
 | |
|         ## Add current temperature to the image
 | |
|         tempString = f"{self.current_weather['temp_feels_like']:.0f}{self.tempDispUnit}"
 | |
|         tempFont = self.get_font("Bold", 68)
 | |
|         # Get the width of the text
 | |
|         tempStringbbox = tempFont.getbbox(tempString)
 | |
|         tempW = tempStringbbox[2] - tempStringbbox[0]
 | |
|         temp_x = int((self.left_section_width - tempW) / 2)
 | |
|         temp_y = int(self.height * 0.4375)
 | |
|         # Draw the current temp centered
 | |
|         image_draw.text((temp_x, temp_y), tempString, font=tempFont, fill=(255, 255, 255))
 | |
| 
 | |
|         # Add icon for rain forecast
 | |
|         rainIcon = Image.open(os.path.join(icons_dir, "rain-chance.bmp"))
 | |
|         rainIcon = rainIcon.resize((40, 40))
 | |
|         rain_y = int(self.height * 0.625)
 | |
|         self.image.paste(rainIcon, (15, rain_y))
 | |
| 
 | |
|         # Amount of precipitation within next 3h
 | |
|         rain = self.hourly_forecasts[0]["precip_3h_mm"]
 | |
|         precipString = f"{rain:.1g} mm" if rain > 0.0 else "0 mm"
 | |
|         precipFont = self.get_font("Bold", self.font_size + 8)
 | |
|         image_draw.text((65, rain_y), precipString, font=precipFont, fill=(255, 255, 255))
 | |
| 
 | |
|         # Add icon for wind speed
 | |
|         windIcon = Image.open(os.path.join(icons_dir, "wind.bmp"))
 | |
|         windIcon = windIcon.resize((40, 40))
 | |
|         wind_y = int(self.height * 0.719)
 | |
|         self.image.paste(windIcon, (15, wind_y))
 | |
| 
 | |
|         # Max. wind speed within next 3h
 | |
|         wind_gust = f"{self.hourly_forecasts[0]['wind_gust']:.0f}"
 | |
|         wind = f"{self.hourly_forecasts[0]['wind']:.0f}"
 | |
|         if self.wind_gusts:
 | |
|             if wind == wind_gust:
 | |
|                 windString = f"{wind} {self.windDispUnit}"
 | |
|             else:
 | |
|                 windString = f"{wind} - {wind_gust} {self.windDispUnit}"
 | |
|         else:
 | |
|             windString = f"{wind} {self.windDispUnit}"
 | |
| 
 | |
|         windFont = self.get_font("Bold", self.font_size + 8)
 | |
|         image_draw.text((65, wind_y), windString, font=windFont, fill=(255, 255, 255))
 | |
| 
 | |
|     def addHourlyForecast(self):
 | |
|         """
 | |
|         Adds a plot for temperature and amount of rain for the upcoming hours to the upper right section
 | |
|         """
 | |
|         ## Create drawing object for image
 | |
|         image_draw = ImageDraw.Draw(self.image)
 | |
| 
 | |
|         ## Draw hourly chart title
 | |
|         title_x = self.left_section_width + 20  # X-coordinate of the title
 | |
|         title_y = 5
 | |
|         chartTitleFont = self.get_font("ExtraBold", self.font_size)
 | |
|         image_draw.text((title_x, title_y), self.chart_title, font=chartTitleFont, fill=0)
 | |
| 
 | |
|         ## Plot the data
 | |
|         # Define the chart parameters
 | |
|         w, h = int(0.75 * self.width), int(0.45 * self.height)  # Width and height of the graph
 | |
| 
 | |
|         # Length of our time axis
 | |
|         num_ticks_x = 22  # ticks*3 hours
 | |
|         timestamps = [item["datetime"] for item in self.hourly_forecasts][:num_ticks_x]
 | |
|         temperatures = np.array([item["temp"] for item in self.hourly_forecasts])[:num_ticks_x]
 | |
|         precipitation = np.array([item["precip_3h_mm"] for item in self.hourly_forecasts])[:num_ticks_x]
 | |
| 
 | |
|         # Create the figure
 | |
|         fig, ax1 = plt.subplots(figsize=(w / self.dpi, h / self.dpi), dpi=self.dpi)
 | |
| 
 | |
|         # Plot Temperature as line plot in red
 | |
|         ax1.plot(timestamps, temperatures, marker=".", linestyle="-", color="r")
 | |
|         temp_base = 3 if self.temp_unit == "celsius" else 5
 | |
|         fig.gca().yaxis.set_major_locator(ticker.MultipleLocator(base=temp_base))
 | |
|         ax1.tick_params(axis="y", colors="red")
 | |
|         ax1.set_yticks(ax1.get_yticks())
 | |
|         ax1.set_yticklabels([f"{int(value)}{self.tempDispUnit}" for value in ax1.get_yticks()])
 | |
|         ax1.grid(visible=True, axis="both")  # Adding grid
 | |
| 
 | |
|         if self.min_max_annotations == True:
 | |
|             # Calculate min_temp and max_temp values based on the minimum and maximum temperatures in the hourly data
 | |
|             min_temp = np.min(temperatures)
 | |
|             max_temp = np.max(temperatures)
 | |
|             # Find positions of min and max values
 | |
|             min_temp_index = np.argmin(temperatures)
 | |
|             max_temp_index = np.argmax(temperatures)
 | |
|             ax1.text(
 | |
|                 timestamps[min_temp_index],
 | |
|                 min_temp,
 | |
|                 f"Min: {min_temp:.1f}{self.tempDispUnit}",
 | |
|                 ha="left",
 | |
|                 va="top",
 | |
|                 color="blue",
 | |
|                 fontsize=12,
 | |
|             )
 | |
|             ax1.text(
 | |
|                 timestamps[max_temp_index],
 | |
|                 max_temp,
 | |
|                 f"Max: {max_temp:.1f}{self.tempDispUnit}",
 | |
|                 ha="left",
 | |
|                 va="bottom",
 | |
|                 color="red",
 | |
|                 fontsize=12,
 | |
|             )
 | |
| 
 | |
|         # Create the second part of the plot as a bar chart for amount of precipitation
 | |
|         ax2 = ax1.twinx()
 | |
|         width = np.min(np.diff(mdates.date2num(timestamps)))
 | |
|         ax2.bar(timestamps, precipitation, color="blue", width=width, alpha=0.2)
 | |
|         ax2.tick_params(axis="y", colors="blue")
 | |
|         ax2.set_ylim([0, 10])
 | |
|         ax2.set_yticks(ax2.get_yticks())
 | |
|         ax2.set_yticklabels([f"{value:.0f}" for value in ax2.get_yticks()])
 | |
| 
 | |
|         fig.gca().xaxis.set_major_locator(mdates.DayLocator(interval=1, tz=self.tz))
 | |
|         fig.gca().xaxis.set_major_formatter(mdates.DateFormatter(fmt="%a", tz=self.tz))
 | |
|         fig.gca().xaxis.set_minor_locator(mdates.HourLocator(interval=3, tz=self.tz))
 | |
|         fig.tight_layout()  # Adjust layout to prevent clipping of labels
 | |
| 
 | |
|         # Get image from plot and add it to the image
 | |
|         hourly_forecast_plot = get_image_from_plot(plt)
 | |
|         plot_x = self.left_section_width + 5
 | |
|         plot_y = title_y + 30
 | |
|         self.image.paste(hourly_forecast_plot, (plot_x, plot_y))
 | |
| 
 | |
|     def addDailyForecast(self):
 | |
|         """
 | |
|         Adds daily weather forecasts to the lower right section
 | |
|         """
 | |
|         ## Create drawing object for image
 | |
|         image_draw = ImageDraw.Draw(self.image)
 | |
| 
 | |
|         ## Draw daily chart title
 | |
|         title_y = int(self.height / 2)  # Y-coordinate of the title
 | |
|         chartTitleFont = self.get_font("Bold", self.font_size)
 | |
|         image_draw.text((self.left_section_width + 20, title_y), self.weekly_title, font=chartTitleFont, fill=0)
 | |
| 
 | |
|         # Define the parameters
 | |
|         number_of_forecast_days = 5  # including today
 | |
|         # Spread evenly, starting from title width
 | |
|         rectangle_width = int((self.width - (self.left_section_width + 40)) / number_of_forecast_days)
 | |
|         # Maximum height for each rectangle (avoid overlapping with title)
 | |
|         rectangle_height = int(self.height / 2 - 20)
 | |
| 
 | |
|         # Rain icon is static
 | |
|         rainIcon = Image.open(os.path.join(icons_dir, "rain-chance.bmp"))
 | |
|         rainIcon.convert("L")
 | |
|         rainIcon = ImageOps.invert(rainIcon)
 | |
|         weeklyRainIcon = rainIcon.resize((20, 20))
 | |
| 
 | |
|         # Loop through the upcoming days' data and create rectangles
 | |
|         for i in range(number_of_forecast_days):
 | |
|             x_rect = self.left_section_width + 20 + i * rectangle_width  # Start from the title width
 | |
|             y_rect = int(self.height / 2 + 30)
 | |
| 
 | |
|             day_data = self.my_owm.get_forecast_for_day(days_from_today=i)
 | |
|             rect = Image.new("RGBA", (int(rectangle_width), int(rectangle_height)), (255, 255, 255))
 | |
|             rect_draw = ImageDraw.Draw(rect)
 | |
| 
 | |
|             # Date string: Day of week on line 1, date on line 2
 | |
|             short_day_font = self.get_font("ExtraBold", self.font_size + 4)
 | |
|             short_month_day_font = self.get_font("Bold", self.font_size - 4)
 | |
|             short_day_name = day_data["datetime"].strftime("%a")
 | |
|             short_month_day = day_data["datetime"].strftime("%b %d")
 | |
|             short_day_name_text = rect_draw.textbbox((0, 0), short_day_name, font=short_day_font)
 | |
|             short_month_day_text = rect_draw.textbbox((0, 0), short_month_day, font=short_month_day_font)
 | |
|             day_name_x = (rectangle_width - short_day_name_text[2] + short_day_name_text[0]) / 2
 | |
|             short_month_day_x = (rectangle_width - short_month_day_text[2] + short_month_day_text[0]) / 2
 | |
|             rect_draw.text((day_name_x, 0), short_day_name, fill=0, font=short_day_font)
 | |
|             rect_draw.text(
 | |
|                 (short_month_day_x, 30),
 | |
|                 short_month_day,
 | |
|                 fill=0,
 | |
|                 font=short_month_day_font,
 | |
|             )
 | |
| 
 | |
|             ## Min and max temperature split into diagonal placement
 | |
|             min_temp = day_data["temp_min"]
 | |
|             max_temp = day_data["temp_max"]
 | |
|             temp_text_min = f"{min_temp:.0f}{self.tempDispUnit}"
 | |
|             temp_text_max = f"{max_temp:.0f}{self.tempDispUnit}"
 | |
|             rect_temp_font = self.get_font("ExtraBold", self.font_size + 4)
 | |
|             temp_x_offset = 20
 | |
|             # this is upper left: max temperature
 | |
|             temp_text_max_x = temp_x_offset
 | |
|             temp_text_max_y = int(rectangle_height * 0.25)
 | |
|             # this is lower right: min temperature
 | |
|             temp_text_min_bbox = rect_draw.textbbox((0, 0), temp_text_min, font=rect_temp_font)
 | |
|             temp_text_min_x = (
 | |
|                 int((rectangle_width - temp_text_min_bbox[2] + temp_text_min_bbox[0]) / 2) + temp_x_offset + 7
 | |
|             )
 | |
|             temp_text_min_y = int(rectangle_height * 0.33)
 | |
|             rect_draw.text((temp_text_min_x, temp_text_min_y), temp_text_min, fill=0, font=rect_temp_font)
 | |
|             rect_draw.text(
 | |
|                 (temp_text_max_x, temp_text_max_y),
 | |
|                 temp_text_max,
 | |
|                 fill=0,
 | |
|                 font=rect_temp_font,
 | |
|             )
 | |
| 
 | |
|             # Weather icon for the day
 | |
|             icon_code = day_data["icon"]
 | |
|             icon = get_weather_icon(icon_name=icon_code, size=90)
 | |
|             if self.icon_outline:
 | |
|                 icon = outline(image=icon, size=8, color=(0, 0, 0, 255))
 | |
|             icon_x = int((rectangle_width - icon.width) / 2)
 | |
|             icon_y = int(rectangle_height * 0.4)
 | |
|             # Create a mask from the alpha channel of the weather icon
 | |
|             if len(icon.split()) == 4:
 | |
|                 mask = icon.split()[-1]
 | |
|             else:
 | |
|                 mask = None
 | |
|             # Paste the foreground of the icon onto the background with the help of the mask
 | |
|             rect.paste(icon, (int(icon_x), icon_y), mask)
 | |
| 
 | |
|             ## Precipitation icon and text
 | |
|             rain = day_data["precip_mm"]
 | |
|             if rain:
 | |
|                 rain_text = f"{rain:.0f} mm"
 | |
|                 rain_font = self.get_font("ExtraBold", self.font_size)
 | |
|                 # Icon
 | |
|                 rain_icon_x = int((rectangle_width - icon.width) / 2)
 | |
|                 rain_icon_y = int(rectangle_height * 0.82)
 | |
|                 rect.paste(weeklyRainIcon, (rain_icon_x, rain_icon_y))
 | |
|                 # Text
 | |
|                 rain_text_y = int(rectangle_height * 0.8)
 | |
|                 rect_draw.text(
 | |
|                     (rain_icon_x + weeklyRainIcon.width + 10, rain_text_y),
 | |
|                     rain_text,
 | |
|                     fill=0,
 | |
|                     font=rain_font,
 | |
|                     align="right",
 | |
|                 )
 | |
| 
 | |
|             self.image.paste(rect, (int(x_rect), int(y_rect)))
 | |
| 
 | |
|     def generate_image(self):
 | |
|         """Generate image for this module"""
 | |
| 
 | |
|         # Define new image size with respect to padding
 | |
|         im_width = int(self.width - (2 * self.padding_left))
 | |
|         im_height = int(self.height - (2 * self.padding_top))
 | |
|         im_size = im_width, im_height
 | |
|         logger.info(f"Image size: {im_size}")
 | |
| 
 | |
|         # Check if internet is available
 | |
|         if internet_available():
 | |
|             logger.info("Connection test passed")
 | |
|         else:
 | |
|             raise NetworkNotReachableError
 | |
| 
 | |
|         # Get the weather
 | |
|         self.my_owm = OpenWeatherMap(
 | |
|             api_key=self.api_key,
 | |
|             api_version=self.owm_api_version,
 | |
|             lat=self.location_lat,
 | |
|             lon=self.location_lon,
 | |
|             temp_unit=self.temp_unit,
 | |
|             wind_unit=self.wind_unit,
 | |
|             language=self.language,
 | |
|             tz_name=self.tz,
 | |
|         )
 | |
|         self.current_weather = self.my_owm.get_current_weather()
 | |
|         self.hourly_forecasts = self.my_owm.get_weather_forecast()
 | |
| 
 | |
|         ## Create Base Image
 | |
|         self.createBaseImage()
 | |
| 
 | |
|         ## Add Current Weather to the left section
 | |
|         self.addCurrentWeather()
 | |
| 
 | |
|         ## Add user-configurable section to the bottom left corner
 | |
|         self.addUserSection()
 | |
| 
 | |
|         ## Add Hourly Forecast to the top right section
 | |
|         self.addHourlyForecast()
 | |
| 
 | |
|         ## Add Daily Forecast to the bottom right section
 | |
|         self.addDailyForecast()
 | |
| 
 | |
|         if self.orientation == "horizontal":
 | |
|             self.image = self.image.rotate(90, expand=True)
 | |
| 
 | |
|         logger.info("Fullscreen weather forecast generated successfully.")
 | |
|         # Convert images according to specified palette
 | |
|         im_black, im_colour = image_to_palette(image=self.image, palette="bwr", dither=True)
 | |
| 
 | |
|         # Return the images ready for the display
 | |
|         return im_black, im_colour
 | |
| 
 | |
|     def get_font(self, style, size):
 | |
|         # Returns the TrueType font object with the given characteristics
 | |
|         # Some workarounds for typefaces that do not exist in some fonts out there
 | |
|         if self.font == "Roboto" and style == "ExtraBold":
 | |
|             style = "Black"
 | |
|         elif self.font in ["Ubuntu", "NotoSansUI"] and style in ["ExtraBold", "Black"]:
 | |
|             style = "Bold"
 | |
|         elif self.font == "OpenSans" and style == "Black":
 | |
|             style = "ExtraBold"
 | |
|         return ImageFont.truetype(fonts[f"{self.font}-{style}"], size=size)
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     print(f"running {__name__} in standalone mode")
 | 
