step 1: actual conditions working
This commit is contained in:
parent
2abc15652c
commit
d2d7c91bb5
26
.pre-commit-config.yaml
Normal file
26
.pre-commit-config.yaml
Normal file
@ -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
|
BIN
icons/ui-icons/home_temp.png
Normal file
BIN
icons/ui-icons/home_temp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
BIN
icons/ui-icons/humidity.bmp
Normal file
BIN
icons/ui-icons/humidity.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
icons/ui-icons/outline_thermostat_white_48dp.bmp
Normal file
BIN
icons/ui-icons/outline_thermostat_white_48dp.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
icons/ui-icons/rain-chance.bmp
Normal file
BIN
icons/ui-icons/rain-chance.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
icons/ui-icons/uv.bmp
Normal file
BIN
icons/ui-icons/uv.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
BIN
icons/ui-icons/wind.bmp
Normal file
BIN
icons/ui-icons/wind.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
0
icons/weather_icons/__init__.py
Normal file
0
icons/weather_icons/__init__.py
Normal file
28
icons/weather_icons/weather_icons.py
Normal file
28
icons/weather_icons/weather_icons.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import os
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
def get_weather_icon(icon_name, size) -> 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
|
@ -13,6 +13,7 @@ import inkycal.modules.inkycal_slideshow
|
|||||||
import inkycal.modules.inkycal_stocks
|
import inkycal.modules.inkycal_stocks
|
||||||
import inkycal.modules.inkycal_webshot
|
import inkycal.modules.inkycal_webshot
|
||||||
import inkycal.modules.inkycal_xkcd
|
import inkycal.modules.inkycal_xkcd
|
||||||
|
import inkycal.modules.inkycal_fullweather
|
||||||
|
|
||||||
# Main file
|
# Main file
|
||||||
from inkycal.main import Inkycal
|
from inkycal.main import Inkycal
|
||||||
|
@ -35,7 +35,7 @@ for path, dirs, files in os.walk(fonts_location):
|
|||||||
if _.endswith('.ttf'):
|
if _.endswith('.ttf'):
|
||||||
name = _.split('.ttf')[0]
|
name = _.split('.ttf')[0]
|
||||||
fonts[name] = os.path.join(path, _)
|
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()]
|
available_fonts = [key for key, values in fonts.items()]
|
||||||
|
|
||||||
|
|
||||||
|
141
inkycal/custom/owm_forecasts.py
Normal file
141
inkycal/custom/owm_forecasts.py
Normal file
@ -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()
|
@ -10,3 +10,4 @@ from .inkycal_slideshow import Slideshow
|
|||||||
from .inkycal_textfile_to_display import TextToDisplay
|
from .inkycal_textfile_to_display import TextToDisplay
|
||||||
from .inkycal_webshot import Webshot
|
from .inkycal_webshot import Webshot
|
||||||
from .inkycal_xkcd import Xkcd
|
from .inkycal_xkcd import Xkcd
|
||||||
|
from .inkycal_fullweather import Fullweather
|
||||||
|
442
inkycal/modules/inkycal_fullweather.py
Normal file
442
inkycal/modules/inkycal_fullweather.py
Normal file
@ -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")
|
@ -1,33 +1,52 @@
|
|||||||
|
appdirs==1.4.4
|
||||||
arrow==1.3.0
|
arrow==1.3.0
|
||||||
|
asyncio==3.4.3
|
||||||
|
beautifulsoup4==4.12.2
|
||||||
certifi==2023.7.22
|
certifi==2023.7.22
|
||||||
|
charset-normalizer==3.3.2
|
||||||
|
colorzero==2.0
|
||||||
|
contourpy==1.2.0
|
||||||
cycler==0.12.1
|
cycler==0.12.1
|
||||||
feedparser==6.0.10
|
feedparser==6.0.10
|
||||||
fonttools==4.45.1
|
fonttools==4.45.1
|
||||||
|
frozendict==2.4.0
|
||||||
|
geojson==2.5.0
|
||||||
gpiozero==2.0
|
gpiozero==2.0
|
||||||
|
html2text==2020.1.16
|
||||||
|
html5lib==1.1
|
||||||
|
htmlwebshot==0.1.2
|
||||||
icalendar==5.0.11
|
icalendar==5.0.11
|
||||||
|
idna==3.6
|
||||||
kiwisolver==1.4.5
|
kiwisolver==1.4.5
|
||||||
lgpio==0.0.0.2
|
lgpio==0.0.0.2
|
||||||
lxml==4.9.3
|
lxml==4.9.3
|
||||||
matplotlib==3.8.2
|
matplotlib==3.8.2
|
||||||
|
multitasking==0.0.11
|
||||||
numpy==1.26.2
|
numpy==1.26.2
|
||||||
packaging==23.2
|
packaging==23.2
|
||||||
|
pandas==2.1.4
|
||||||
|
peewee==3.17.0
|
||||||
Pillow==10.1.0
|
Pillow==10.1.0
|
||||||
|
pyowm==3.3.0
|
||||||
pyparsing==3.1.1
|
pyparsing==3.1.1
|
||||||
PySocks==1.7.1
|
PySocks==1.7.1
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
|
python-dotenv==1.0.0
|
||||||
pytz==2023.3.post1
|
pytz==2023.3.post1
|
||||||
recurring-ical-events==2.1.1
|
recurring-ical-events==2.1.1
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
RPi.GPIO==0.7.1
|
RPi.GPIO==0.7.1
|
||||||
sgmllib3k==1.0.0
|
sgmllib3k==1.0.0
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
|
soupsieve==2.5
|
||||||
spidev==3.5
|
spidev==3.5
|
||||||
todoist-api-python==2.1.3
|
todoist-api-python==2.1.3
|
||||||
|
types-python-dateutil==2.8.19.20240106
|
||||||
typing_extensions==4.8.0
|
typing_extensions==4.8.0
|
||||||
|
tzdata==2023.4
|
||||||
|
tzlocal==5.2
|
||||||
urllib3==2.1.0
|
urllib3==2.1.0
|
||||||
python-dotenv==1.0.0
|
webencodings==0.5.1
|
||||||
setuptools==69.0.2
|
x-wr-timezone==0.0.6
|
||||||
html2text==2020.1.16
|
|
||||||
yfinance==0.2.32
|
|
||||||
htmlwebshot~=0.1.2
|
|
||||||
xkcd==2.4.2
|
xkcd==2.4.2
|
||||||
|
yfinance==0.2.32
|
||||||
|
Loading…
Reference in New Issue
Block a user