# display/image_maker.py from PIL import Image, ImageDraw, ImageFont, ImageOps from fonts import Font from icons import Icon from .epd4in2 import EPD_HEIGHT as HEIGHT from .epd4in2 import EPD_WIDTH as WIDTH def closest(num_list: list, K): return num_list[min(range(len(num_list)), key=lambda i: abs(num_list[i] - K))] def draw_temp_block( d: ImageDraw.ImageDraw, coords: tuple[int, int], content: list[tuple[str, int, ImageFont.FreeTypeFont]], ): x, y = coords for text, color, font in content: d.text((x, y), text, fill=color, font=font) text_width = int(font.getlength(text)) x += text_width class eInkImage: def __init__(self): self.info_font_y_offset = 1 self.info_y_offset = 14 def make_image( self, date: tuple, weather_data: dict, network_status: bool ) -> Image.Image: forecast = weather_data.get("forecast", {}) image = Image.new("1", (WIDTH, HEIGHT), 255) draw = ImageDraw.Draw(image) # Top draw.text((6, 2), date[0], font=Font.f20, fill=0) draw.text((6, 20), date[1], font=Font.f60, fill=0) # --- Left-side --- image.paste(Icon.default["indoor"], (6, 78 + self.info_y_offset)) draw.text( (36, 78 - self.info_font_y_offset + self.info_y_offset), "Inomhus", font=Font.f20, fill=0, ) # Temp Logic indoor_temp = weather_data.get("indoor_temp", 20) if indoor_temp >= 24.0: indoor_temp_icon = Icon.default["temp_high"] elif indoor_temp <= 18.0: indoor_temp_icon = Icon.default["temp_low"] else: indoor_temp_icon = Icon.default["temp_mid"] image.paste(indoor_temp_icon, (6, 116 + self.info_y_offset)) draw_temp_block( draw, (38, 93 + self.info_y_offset), [ (str(weather_data.get("indoor_temp", "--")), 0, Font.f55), ("°C", 0, Font.f25), ], ) # ---------- image.paste(Icon.default["humidity"], (6, 160 + self.info_y_offset)) draw.text( (36, 160 - self.info_font_y_offset + self.info_y_offset), f"{weather_data.get('indoor_humidity', '--')}%", font=Font.f20, fill=0, ) # ---------- image.paste(Icon.default["pressure"], (6, 190 + self.info_y_offset)) draw.text( (36, 190 - self.info_font_y_offset + self.info_y_offset), f"{weather_data.get('pressure', '--')}hPa", font=Font.f20, fill=0, ) # Sun image.paste(Icon.default["sunrise"], (6, 220 + self.info_y_offset)) draw.text( (36, 220 - self.info_font_y_offset + self.info_y_offset), weather_data.get("sunrise", "--"), font=Font.f20, fill=0, ) # ---------- image.paste(Icon.default["sunset"], (101, 220 + self.info_y_offset)) draw.text( (131, 220 - self.info_font_y_offset + self.info_y_offset), weather_data.get("sunset", "--"), font=Font.f20, fill=0, ) # Moon moon_value = closest( [direction for direction, _ in Icon.moon.items()], weather_data.get("moon_illumination", 0.167) * 100, ) moon_icon = Icon.moon[moon_value] if not weather_data.get("moon_to_full", True): image.paste(moon_icon, (6, 250 + self.info_y_offset)) else: image.paste(ImageOps.mirror(moon_icon), (6, 250 + self.info_y_offset)) draw.text( (42, 250 + self.info_y_offset), weather_data.get("moon_phase", "----"), font=Font.f20, fill=0, ) # Right-side right_side_x = 204 if weather_data.get("temperature", 10) >= 24.0: outdoor_temp_icon = Icon.default["temp_high"] elif weather_data.get("temperature", 10) < 0.0: outdoor_temp_icon = Icon.default["temp_low"] else: outdoor_temp_icon = Icon.default["temp_mid"] image.paste(outdoor_temp_icon, (right_side_x, 116 + self.info_y_offset)) draw_temp_block( draw, (right_side_x + 30, 93 + self.info_y_offset), [ (str(weather_data.get("temperature", "--")), 0, Font.f55), ("°C", 0, Font.f25), ], ) # ---------- image.paste(Icon.default["humidity"], (right_side_x, 160 + self.info_y_offset)) draw.text( (right_side_x + 30, 160 - self.info_font_y_offset + self.info_y_offset), f"{weather_data.get('humidity', '--')}%", font=Font.f20, fill=0, ) # ---------- image.paste(Icon.default["dew"], (right_side_x + 96, 160 + self.info_y_offset)) draw.text( (right_side_x + 126, 160 - self.info_font_y_offset + self.info_y_offset), f"{weather_data.get('dew', '--')}°C", font=Font.f20, fill=0, ) # Wind wind_offset = 20 wind_deg = forecast.get("wind", {}).get("deg", 0) wind_speed = forecast.get("wind", {}).get("speed", "--") wind_gust = forecast.get("wind", {}).get("gust", "--") has_wind = bool(forecast.get("wind")) wind_value = closest( [direction for direction, _ in Icon.wind.items()], wind_deg ) wind_direction, wind_icon = ( Icon.wind[wind_value] if has_wind else ("--", Icon.wind[180][1]) ) direction_width = int(Font.f14.getlength(wind_direction)) wind_direction_text_x = ( right_side_x + wind_offset + ((30 - direction_width) / 2) ) image.paste(wind_icon, (right_side_x + wind_offset, 192 + self.info_y_offset)) image.paste( Icon.default["wind"], (right_side_x + 40 + wind_offset, 190 + self.info_y_offset), ) image.paste( Icon.default["wind_gust"], (right_side_x + 40 + wind_offset, 210 + self.info_y_offset), ) draw.text( (wind_direction_text_x, 222 + self.info_y_offset), wind_direction, font=Font.f14, fill=0, ) draw.text( ( right_side_x + 70 + wind_offset, 190 - self.info_font_y_offset + self.info_y_offset, ), f"{wind_speed}{'m/s' if has_wind else ''}", font=Font.f20, fill=0, ) draw.text( ( right_side_x + 70 + wind_offset, 210 - self.info_font_y_offset + self.info_y_offset, ), f"{wind_gust}{'m/s' if has_wind else ''}", font=Font.f20, fill=0, ) # UV image.paste(Icon.default["uv"], (right_side_x + 60, 240 + self.info_y_offset)) draw.text( ( right_side_x + 60 + 30, 240 - self.info_font_y_offset + self.info_y_offset, ), str(weather_data.get("uv_index", "--")), font=Font.f20, fill=0, ) # Warnings if not network_status: image.paste( Icon.warning["no_wifi"], (WIDTH - Icon.warning["no_wifi"].width - 4, 80), ) if ( forecast.get("wind", {}).get("speed", 0) >= 14.0 or forecast.get("wind", {}).get("gust", 0) >= 16.0 ): image.paste( Icon.warning["wind"], (250 - Icon.warning["wind"].width - 4, 80) ) if weather_data.get("fire_index", 0) > 3: image.paste( Icon.warning["fire"], (WIDTH - Icon.warning["fire"].width - 4, 4) ) alerts = weather_data.get("weather_alerts") if alerts and isinstance(alerts, list) and len(alerts) > 0: warning_code = alerts[0].get("type") if warning_code in ["RED", "ORANGE", "YELLOW"]: image.paste( Icon.warning[warning_code], (WIDTH - Icon.warning[warning_code].width - 4, 80), ) elif warning_code == "MESSAGE": if warning_code == "WATER_SHORTAGE": image.paste( Icon.warning["water_shortage"], (WIDTH - Icon.warning["water_shortage"].width - 4, 80), ) elif warning_code == "HIGH_TEMPERATURES": image.paste( Icon.warning["temperature_high"], (WIDTH - Icon.warning["temperature_high"].width - 4, 80), ) else: image.paste( Icon.warning["YELLOW"], (WIDTH - Icon.warning["YELLOW"].width - 4, 80), ) # Weather-icon image = image.convert("L") if ( weather_data.get("sunrise", "06:00") < date[1] < weather_data.get("sunset", "20:00") ): time_of_day = "day" else: time_of_day = "night" weather_icon = Icon.get_forecast( forecast.get("weather_symbol", 0), time_of_day, forecast.get("wind", {}).get("speed", 0), ) weather_icon_y_offset = int(((120 - weather_icon.size[1]) / 2)) image.paste(weather_icon, (250, weather_icon_y_offset), weather_icon) return image