From d2d7c91bb5de3956ede027e9e5dcb9862bc50778 Mon Sep 17 00:00:00 2001 From: mrbwburns <> Date: Mon, 15 Jan 2024 19:08:46 +0100 Subject: [PATCH] step 1: actual conditions working --- .pre-commit-config.yaml | 26 ++ icons/ui-icons/home_temp.png | Bin 0 -> 4635 bytes icons/ui-icons/humidity.bmp | Bin 0 -> 1216 bytes .../outline_thermostat_white_48dp.bmp | Bin 0 -> 1216 bytes icons/ui-icons/rain-chance.bmp | Bin 0 -> 12064 bytes icons/ui-icons/uv.bmp | Bin 0 -> 64080 bytes icons/ui-icons/wind.bmp | Bin 0 -> 1216 bytes icons/weather_icons/__init__.py | 0 icons/weather_icons/weather_icons.py | 28 ++ inkycal/__init__.py | 1 + inkycal/custom/functions.py | 2 +- inkycal/custom/owm_forecasts.py | 141 ++++++ inkycal/modules/__init__.py | 1 + inkycal/modules/inkycal_fullweather.py | 442 ++++++++++++++++++ requirements.txt | 29 +- 15 files changed, 664 insertions(+), 6 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 icons/ui-icons/home_temp.png create mode 100644 icons/ui-icons/humidity.bmp create mode 100644 icons/ui-icons/outline_thermostat_white_48dp.bmp create mode 100644 icons/ui-icons/rain-chance.bmp create mode 100644 icons/ui-icons/uv.bmp create mode 100644 icons/ui-icons/wind.bmp create mode 100644 icons/weather_icons/__init__.py create mode 100644 icons/weather_icons/weather_icons.py create mode 100644 inkycal/custom/owm_forecasts.py create mode 100644 inkycal/modules/inkycal_fullweather.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..3c49c00 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +repos: + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + args: + - "--line-length=120" + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v1.4.0 + hooks: + - id: trailing-whitespace + - id: check-docstring-first + - id: check-json + - id: check-yaml + - id: debug-statements + - id: flake8 + args: + - "--ignore=E, W" + - repo: https://github.com/asottile/reorder_python_imports + rev: v1.1.0 + hooks: + - id: reorder-python-imports + - repo: meta + hooks: + - id: check-hooks-apply + - id: check-useless-excludes diff --git a/icons/ui-icons/home_temp.png b/icons/ui-icons/home_temp.png new file mode 100644 index 0000000000000000000000000000000000000000..afcd5fce1d8371b6a2e682a1b9376e5edb916474 GIT binary patch literal 4635 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 R&&dD)002ovPDHLkV1k-)`LO^1 literal 0 HcmV?d00001 diff --git a/icons/ui-icons/humidity.bmp b/icons/ui-icons/humidity.bmp new file mode 100644 index 0000000000000000000000000000000000000000..7248bbb0f859b9f522b238a320062ab711a70042 GIT binary patch literal 1216 zcmcJOF>Zx042FrQl%WGsFOapTXr%7FLvNC(OUHL5I&lr8cLBOFAl(1A9Ue=EN_`Q5 z4=45?8}s@$K6E->aDT>p_ZhkX>beiCKRw!1gWp8t{CYn==|qva31yM3G0xHs)00PA z!1lu-kyo>4vO5>qahYAD1`iZyD^j@-=$DLpnadJQAj+34FmTTxQI-xDc~5|%oVEjC}DgF2x{5N^=GV8|!uj3aV#(8e_m`k@P)YiNv~@>XJLW^XYHFN4d%aZN6ZfLnGc z2<^qCAa=Al#aehCo+;SCPck+}$JfT)@f*QXj_|ikxG{MqGnS&ot;c+3#`s{BZPOQDM)0h!M)Sdu0A z{^*agd^cUMYR}Dr1SOHT{=b}8TZC-^z}t45KkhpKFWa1e!8iiNI02J!2K=z*>j@L9 zOLuyhxnB=6YbX!KtYJOy-k9_J#e7F*u8GWdw0bgUs%@g0b1#~3iQenjgzGHD@(=GU opRud;VV+XeeQZUzM>;5j^_p3Sqc@%3pbm%0BJ@tB8>`-wzSUq2)&Kwi literal 0 HcmV?d00001 diff --git a/icons/ui-icons/rain-chance.bmp b/icons/ui-icons/rain-chance.bmp new file mode 100644 index 0000000000000000000000000000000000000000..dacb1fe08128b6f81a92df23f3ba1e6ec8c2d7d6 GIT binary patch literal 12064 zcmeI1JB}nb42F9!!3GWn@CD@1fnndk1#Ip!Nk^-r2tI*B2Md?^lOjburv{$&0*fhN zyY%?<^C6b1r}y(8KmGRL>v#J6ng0FqB>$cs)S&CnUmuSje~^DXP(H5f^4*X2M+UxS z23~Tp{``-+aU^ZId;Gg}!d0f9uI%PZ^XXEVL#IDoy42y%_7ueHFByNizBr&XKV53> zQkh^T9R6}$CwHGNl{w6W-1Li2ieIa(}wLG>u0iQY{P&WPTjdKCDmcb9`Rq8C+<0w49jNuKu{oDq&1lD|N$ z)29yYhLbt~mwFD)WDFu8;f_~1OVD`G2U>hWuL*q4CFQ!nF|3z4(*#rsQ6aj6<1V<& zEozLO&Ca55MBe`xDYT!#QVW#f5t-C;;ym$G@a|bhejk{0- zR|fZnOBewsfumQh|5$#5UUeptIW}U?o!Z2`;r_?+o9tE~N0avrZF3q4l3f7TW6Yfx zt=#b{Cs`wIJk?+~soq^KV*w7txgO8LzsxP-#xg}-j60PZ%a6EXgcb-FG(o(od^B9* zLE?sT!P8TmA-xSp^-?*pL3&wQ?vuFUbkyCVl#U#wqd0SDK;+8lluFaMpbpHrs77AY z0dggcdO-)Db-_a&Xy9ktBP+V2MAmR0osSHBUm3uwp}q1fKza;^>zsfCL<8eF$*=Jd z9R-gOqLJZX!*M&g2E(%`ur76xgozl2a>vC5*E_ht^o�aWBfkE)cx7=9enTJ$EIuwr$m&7E0#tnDgLC^TTE$1g!Vq?pgcRV#kwp zvC1v8asC!uA9Z%aUi+1jFPHTRVQ!2T3t;HfmAq|Pd(Ax#Tx#aE6Nt;&;ce4Sdq=y^i+3B8M6~ z)lPqRcHhFKUPt?#-nZcJjNY*ePTd~vE{=EMLal?X-Wxc5CSKFK#U1rI+Uniq;!Qo` z^jX~6z3y@ToZRW%<^1`))9bla{ZU!j834T%7aRxe6?fd^B5~(KuALrX4X10$QoSL4 zcQ}M-zQgW94{0%Ob50)Myc1bF&7IxTW0!x~nxD@Ra>n7^IdZH8P9G!wlA<+R%^Fvy zXi@DbIpLxgAGqq$p3AxtU+Zd6I%;-L9jX5VVI$O7aLF@h8&@Zet8Su=PPjl^HlQ?= zbDnRxx*hx*X3bKk=XL;9=DGr^-CWku4OQ#px13d@E?2#z1M(%$s#}MZ(>~qtfDBJa z;k-XQl-)u!3)jR4y=I_t8Ef|s=(Cb{W-&Fkg>@6TZw>g|~C8tfUXB__-ME>|lP`qsv z)Qf=VauZYTV5;Z3?2$+)T&jXJG;(}i6Z;y6;BcK)@0jUn0$a|fT?90Nx$4Dn+h~$T zakX4qd@OM==8t_Em~bWFIs;E?&)g{i*BNlb*`!a6D>!6e!j(Yk4#0qm$_yVg?lZgM zN+7ll{h40bk)&WC?$nOIN1AX`hwpe!3}+pR#3k+cyKJ$gg0MOF2$<4D^C~an+L1Ud zB@WBY`6fKnL?eCL_6*vXQ=bAgRlDcd)+wXMYg`GKFpH_z;}|7i*X*fMTjD&sYPsxD z7?VH(TDhOsezaL`ojTs%4{*qa6wi_i;80D{NVH;l$@CX}wWTRUgQcb+O4n-J%Ek$_ zQa$gb1eCT5*J zGUWW*q@_tr-uUN#hdy_l3pnk#S_!*weZZS4fT5@nZ^LyzsGmFcT5#_+!lebY&hGxWdldw4LLmR`#oD)_?Y;}!1tJee*wHh1yKM1 literal 0 HcmV?d00001 diff --git a/icons/ui-icons/uv.bmp b/icons/ui-icons/uv.bmp new file mode 100644 index 0000000000000000000000000000000000000000..12294b710ef64d6f56b3e63690259d7b3efbe500 GIT binary patch literal 64080 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 Image: + """ + Gets the requested weather icon as Image and returns it in the requested size + :param icon_name: + icon_name for the weather + :param size: + size of the icon in pixels + :return: + the resized weather icon + """ + weatherdir = os.path.dirname(os.path.abspath(__file__)) + iconpath = os.path.join(weatherdir, "owm_icons_cache", f"{icon_name}.png") + + if not os.path.exists(iconpath): + urllib.request.urlretrieve( + url=f"https://openweathermap.org/img/wn/{icon_name}@2x.png", filename=f"{iconpath}" + ) + icon = Image.open(iconpath) + + icon = icon.resize((size, size)) + + return icon diff --git a/inkycal/__init__.py b/inkycal/__init__.py index 1ae709a..0c6a244 100644 --- a/inkycal/__init__.py +++ b/inkycal/__init__.py @@ -13,6 +13,7 @@ import inkycal.modules.inkycal_slideshow import inkycal.modules.inkycal_stocks import inkycal.modules.inkycal_webshot import inkycal.modules.inkycal_xkcd +import inkycal.modules.inkycal_fullweather # Main file from inkycal.main import Inkycal diff --git a/inkycal/custom/functions.py b/inkycal/custom/functions.py index 8eddd4e..cf2b1ed 100644 --- a/inkycal/custom/functions.py +++ b/inkycal/custom/functions.py @@ -35,7 +35,7 @@ for path, dirs, files in os.walk(fonts_location): if _.endswith('.ttf'): name = _.split('.ttf')[0] fonts[name] = os.path.join(path, _) - +logs.debug(f"Found fonts: {json.dumps(fonts, indent=4, sort_keys=True)}") available_fonts = [key for key, values in fonts.items()] diff --git a/inkycal/custom/owm_forecasts.py b/inkycal/custom/owm_forecasts.py new file mode 100644 index 0000000..4e864b0 --- /dev/null +++ b/inkycal/custom/owm_forecasts.py @@ -0,0 +1,141 @@ +import logging +from datetime import datetime +from datetime import timedelta + +import arrow +from dateutil import tz +from pyowm import OWM +from pyowm.utils.config import get_default_config + +from inkycal.custom.functions import get_system_tz + +## Configure logger instance for local logging +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + + +def is_timestamp_within_range(timestamp, start_time, end_time): + # Check if the timestamp is within the range + return start_time <= timestamp <= end_time + + +def get_owm_data(city_id: int, token: str, temp_units: str, wind_units: str, language: str): + config_dict = get_default_config() + config_dict["language"] = language + + tz_zone = tz.gettz(get_system_tz()) + + owm = OWM(token, config_dict) + + mgr = owm.weather_manager() + + current_observation = mgr.weather_at_id(id=city_id) + current_weather = current_observation.weather + hourly_forecasts = mgr.forecast_at_id(id=city_id, interval="3h") + + # Forecasts are provided for every 3rd full hour + # - find out how many hours there are until the next 3rd full hour + now = arrow.utcnow() + if (now.hour % 3) != 0: + hour_gap = 3 - (now.hour % 3) + else: + hour_gap = 3 + + # Create timings for hourly forcasts + steps = [i * 3 for i in range(40)] + forecast_timings = [now.shift(hours=+hour_gap + step).floor("hour") for step in steps] + + # Create forecast objects for given timings + forecasts = [hourly_forecasts.get_weather_at(forecast_time.datetime) for forecast_time in forecast_timings] + + # Add forecast-data to fc_data list of dictionaries + hourly_data_dict = [] + for forecast in forecasts: + temp = forecast.temperature(unit=temp_units)["temp"] + min_temp = forecast.temperature(unit=temp_units)["temp_min"] + max_temp = forecast.temperature(unit=temp_units)["temp_max"] + wind = forecast.wind(unit=wind_units)["speed"] + wind_gust = forecast.wind(unit=wind_units)["gust"] + # combined precipitation (snow + rain) + precip_mm = 0.0 + if "3h" in forecast.rain.keys(): + precip_mm = +forecast.rain["3h"] + if "3h" in forecast.snow.keys(): + precip_mm = +forecast.snow["3h"] + + icon = forecast.weather_icon_name + hourly_data_dict.append( + { + "temp": temp, + "min_temp": min_temp, + "max_temp": max_temp, + "precip_3h_mm": precip_mm, + "wind": wind, + "wind_gust": wind_gust, + "icon": icon, + "datetime": forecast_timings[forecasts.index(forecast)].datetime.astimezone(tz=tz_zone), + } + ) + + return (current_weather, hourly_data_dict) + + +def get_forecast_for_day(days_from_today: int, hourly_forecasts: list) -> dict: + """Get temperature range, rain and most frequent icon code for forecast + days_from_today should be int from 0-4: e.g. 2 -> 2 days from today + """ + # Calculate the start and end times for the specified number of days from now + current_time = datetime.now() + tz_zone = tz.gettz(get_system_tz()) + start_time = ( + (current_time + timedelta(days=days_from_today)) + .replace(hour=0, minute=0, second=0, microsecond=0) + .astimezone(tz=tz_zone) + ) + end_time = (start_time + timedelta(days=1)).astimezone(tz=tz_zone) + + # Get all the forecasts for that day's time range + forecasts = [ + f + for f in hourly_forecasts + if is_timestamp_within_range(timestamp=f["datetime"], start_time=start_time, end_time=end_time) + ] + + # if all the forecasts are from the next day, at least use the first one in the list to be able to return something + if forecasts == []: + forecasts.append(hourly_forecasts[0]) + + # Get rain and temperatures for that day + temps = [f["temp"] for f in forecasts] + rain = sum([f["precip_3h_mm"] for f in forecasts]) + + # Get all weather icon codes for this day + icons = [f["icon"] for f in forecasts] + day_icons = [icon for icon in icons if "d" in icon] + + # Use the day icons if possible + icon = max(set(day_icons), key=icons.count) if len(day_icons) > 0 else max(set(icons), key=icons.count) + + # Return a dict with that day's data + day_data = { + "datetime": start_time.timestamp(), + "icon": icon, + "temp_min": min(temps), + "temp_max": max(temps), + "precip_mm": rain, + } + + return day_data + + +def main(): + config_dict = get_default_config() + config_dict["language"] = "en" + token = "daa8543f445b602da5d827e90f1d22b3" + city_id = 2867714 + + print(get_owm_data(city_id=city_id, token=token, temp_units="fahrenheit", wind_units="knots", language="en")) + + +if __name__ == "__main__": + main() diff --git a/inkycal/modules/__init__.py b/inkycal/modules/__init__.py index 3c0d809..7eb656d 100755 --- a/inkycal/modules/__init__.py +++ b/inkycal/modules/__init__.py @@ -10,3 +10,4 @@ from .inkycal_slideshow import Slideshow from .inkycal_textfile_to_display import TextToDisplay from .inkycal_webshot import Webshot from .inkycal_xkcd import Xkcd +from .inkycal_fullweather import Fullweather diff --git a/inkycal/modules/inkycal_fullweather.py b/inkycal/modules/inkycal_fullweather.py new file mode 100644 index 0000000..f98a6eb --- /dev/null +++ b/inkycal/modules/inkycal_fullweather.py @@ -0,0 +1,442 @@ +""" +Inkycal fullscreen weather module +Copyright by mrbwburns +""" +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 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 import owm_forecasts +from inkycal.custom.functions import fonts +from inkycal.custom.functions import internet_available +from inkycal.custom.functions import top_level +from inkycal.custom.inkycal_exceptions import NetworkNotReachableError +from inkycal.modules.template import inkycal_module + +logger = logging.getLogger(__name__) +logger.setLevel("DEBUG") + +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 + + +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", + }, + "location": { + "label": "Please enter your location ID found in the url " + + "e.g. https://openweathermap.org/city/4893171 -> ID is 4893171" + }, + } + + optional = { + "temp_units": { + "label": "Which temperature unit should be used?", + "options": ["celsius", "fahrenheit"], + }, + "wind_units": { + "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"], + }, + "tz": { + "label": "Your timezone", + "options": ["Europe/Berlin", "UTC"], + }, + "font_family": { + "label": "Font family to use for the entire screen", + "options": ["Roboto", "NotoSans", "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"] + + # 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 = int(config["location"]) + self.font_size = int(config["fontsize"]) + + # optional parameters + if "wind_units" in config: + self.wind_units = config["wind_units"] + else: + self.wind_units = "meters_sec" + if self.wind_units == "beaufort": + self.windDispUnit = "bft" + elif self.wind_units == "knots": + self.windDispUnit = "kn" + elif self.wind_units == "km_hour": + self.windDispUnit = "km/h" + elif self.wind_units == "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_units" in config: + self.temp_units = config["temp_units"] + else: + self.temp_units = "celsius" + if self.temp_units == "fahrenheit": + self.tempDispUnit = "F" + elif self.temp_units == "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 "tz" in config: + self.tz = config["tz"] + else: + self.tz = "UTC" + + if "icon_outline" in config: + self.icon_outline = config["icon_outline"] + else: + self.icon_outline = True + + if "font_family" in config: + self.font_family = config["font_family"] + else: + self.font_family = "Roboto" + + # some calculations for scalability + # TODO: make this work for all sizes + 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 + now = datetime.now() + dateString = now.strftime("%d. %B") + dateFont = self.get_font(family=self.font_family, 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(self.font_family, "Bold", 28) + 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 + uvString = f"{self.current_weather.uvi if self.current_weather.uvi else '0'}" + uvFont = self.get_font(self.font_family, "Bold", 28) + 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(self.font_family, "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.temperature(self.temp_units)['feels_like']:.0f}{self.tempDispUnit}" + tempFont = self.get_font(self.font_family, "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(self.font_family, "Bold", 28) + 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(self.font_family, "Bold", 28) + image_draw.text((65, wind_y), windString, font=windFont, fill=(255, 255, 255)) + + 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.current_weather, self.hourly_forecasts) = owm_forecasts.get_owm_data( + token=self.api_key, + city_id=self.location, + temp_units=self.temp_units, + wind_units=self.wind_units, + language=self.language, + ) + + ## 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 + # my_image = addHourlyForecast(display=display, image=my_image, hourly_forecasts=hourly_forecasts) + + ## Add Daily Forecast + # my_image = addDailyForecast(display=display, image=my_image, hourly_forecasts=hourly_forecasts) + + self.image.save("./openweather_full.png") + + logger.info("Fullscreen weather forecast generated successfully.") + # Return the images ready for the display + # tbh, I have no idea why I need to return two separate images here + return self.image, self.image + + @staticmethod + def get_font(family, style, size): + # Returns the TrueType font object with the given characteristics + if family == "Roboto" and style == "ExtraBold": + style = "Black" + elif family == "Ubuntu" and style in ["ExtraBold", "Black"]: + style = "Bold" + elif family == "OpenSans" and style == "Black": + style = "ExtraBold" + return ImageFont.truetype(fonts[f"{family}-{style}"], size=size) + + +if __name__ == "__main__": + print(f"running {__name__} in standalone mode") diff --git a/requirements.txt b/requirements.txt index 9a2a652..bd22fa7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,33 +1,52 @@ +appdirs==1.4.4 arrow==1.3.0 +asyncio==3.4.3 +beautifulsoup4==4.12.2 certifi==2023.7.22 +charset-normalizer==3.3.2 +colorzero==2.0 +contourpy==1.2.0 cycler==0.12.1 feedparser==6.0.10 fonttools==4.45.1 +frozendict==2.4.0 +geojson==2.5.0 gpiozero==2.0 +html2text==2020.1.16 +html5lib==1.1 +htmlwebshot==0.1.2 icalendar==5.0.11 +idna==3.6 kiwisolver==1.4.5 lgpio==0.0.0.2 lxml==4.9.3 matplotlib==3.8.2 +multitasking==0.0.11 numpy==1.26.2 packaging==23.2 +pandas==2.1.4 +peewee==3.17.0 Pillow==10.1.0 +pyowm==3.3.0 pyparsing==3.1.1 PySocks==1.7.1 python-dateutil==2.8.2 +python-dotenv==1.0.0 pytz==2023.3.post1 recurring-ical-events==2.1.1 requests==2.31.0 RPi.GPIO==0.7.1 sgmllib3k==1.0.0 six==1.16.0 +soupsieve==2.5 spidev==3.5 todoist-api-python==2.1.3 +types-python-dateutil==2.8.19.20240106 typing_extensions==4.8.0 +tzdata==2023.4 +tzlocal==5.2 urllib3==2.1.0 -python-dotenv==1.0.0 -setuptools==69.0.2 -html2text==2020.1.16 -yfinance==0.2.32 -htmlwebshot~=0.1.2 +webencodings==0.5.1 +x-wr-timezone==0.0.6 xkcd==2.4.2 +yfinance==0.2.32