Merge branch 'main' into weather_scaling

This commit is contained in:
mrbwburns
2023-11-26 17:16:03 +01:00
committed by GitHub
50 changed files with 1360 additions and 894 deletions

View File

@@ -8,3 +8,5 @@ from .inkycal_jokes import Jokes
from .inkycal_stocks import Stocks
from .inkycal_slideshow import Slideshow
from .inkycal_textfile_to_display import TextToDisplay
from .inkycal_webshot import Webshot
from .inkycal_xkcd import Xkcd

View File

@@ -1,5 +1,3 @@
#!python3
"""
Inkycal iCalendar parsing module
Copyright by aceinnolab
@@ -119,7 +117,7 @@ class iCalendar:
events = (
{
'title': events.get('SUMMARY').lstrip(),
'title': events.get('SUMMARY').lstrip() if events.get('SUMMARY') else "",
'begin': arrow.get(events.get('DTSTART').dt).to(timezone) if (
arrow.get(events.get('dtstart').dt).format('HH:mm') != '00:00')

View File

@@ -83,12 +83,11 @@ class Inkyimage:
@staticmethod
def preview(image):
""""Previews an image on gpicview (only works on Rapsbian with Desktop).
"""
path = '/home/pi/Desktop/'
image.save(path + 'temp.png')
os.system("gpicview " + path + 'temp.png')
os.system('rm ' + path + 'temp.png')
"""Previews an image on gpicview (only works on Rapsbian with Desktop)."""
path = '~/temp'
image.save(path + '/temp.png')
os.system("gpicview " + path + '/temp.png')
os.system('rm ' + path + '/temp.png')
def _image_loaded(self):
"""returns True if image was loaded"""

View File

@@ -1,5 +1,3 @@
#!python3
"""
Inkycal Agenda Module
Copyright by aceinnolab
@@ -98,7 +96,9 @@ class Agenda(inkycal_module):
# Calculate the max number of lines that can fit on the image
line_spacing = 1
line_height = int(self.font.getsize('hg')[1]) + line_spacing
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
line_width = im_width
max_lines = im_height // line_height
logger.debug(f'max lines: {max_lines}')
@@ -109,9 +109,11 @@ class Agenda(inkycal_module):
# Create a list of dates for the next days
agenda_events = [
{'begin': today.shift(days=+_),
'title': today.shift(days=+_).format(
self.date_format, locale=self.language)}
{
'begin': today.shift(days=+_),
'title': today.shift(days=+_).format(
self.date_format, locale=self.language)
}
for _ in range(max_lines)]
# Load icalendar from config
@@ -133,9 +135,9 @@ class Agenda(inkycal_module):
# parser.show_events()
# Set the width for date, time and event titles
date_width = int(max([self.font.getsize(
dates['begin'].format(self.date_format, locale=self.language))[0]
for dates in agenda_events]) * 1.2)
date_width = int(max([self.font.getlength(
dates['begin'].format(self.date_format, locale=self.language))
for dates in agenda_events]) * 1.2)
logger.debug(f'date_width: {date_width}')
# Calculate positions for each line
@@ -147,9 +149,10 @@ class Agenda(inkycal_module):
logger.info('Managed to parse events from urls')
# Find out how much space the event times take
time_width = int(max([self.font.getsize(
events['begin'].format(self.time_format, locale=self.language))[0]
for events in upcoming_events]) * 1.2)
time_width = int(max([self.font.getlength(
events['begin'].format(self.time_format, locale=self.language))
for events in upcoming_events]) * 1.2)
logger.debug(f'time_width: {time_width}')
# Calculate x-pos for time
@@ -224,7 +227,3 @@ class Agenda(inkycal_module):
# return the images ready for the display
return im_black, im_colour
if __name__ == '__main__':
print(f'running {__name__} in standalone mode')

View File

@@ -1,5 +1,3 @@
#!python3
"""
Inkycal Calendar Module
Copyright by aceinnolab
@@ -110,7 +108,8 @@ class Calendar(inkycal_module):
# Allocate space for month-names, weekdays etc.
month_name_height = int(im_height * 0.10)
weekdays_height = int(self.font.getsize('hg')[1] * 1.25)
text_bbox_height = self.font.getbbox("hg")
weekdays_height = int((text_bbox_height[3] - text_bbox_height[1])* 1.25)
logger.debug(f"month_name_height: {month_name_height}")
logger.debug(f"weekdays_height: {weekdays_height}")
@@ -182,15 +181,15 @@ class Calendar(inkycal_module):
]
logger.debug(f'weekday names: {weekday_names}')
for idx, weekday in enumerate(weekday_pos):
for index, weekday in enumerate(weekday_pos):
write(
im_black,
weekday,
(icon_width, weekdays_height),
weekday_names[idx],
weekday_names[index],
font=self.font,
autofit=True,
fill_height=1.0,
fill_height=0.9,
)
# Create a calendar template and flatten (remove nestings)
@@ -207,6 +206,10 @@ class Calendar(inkycal_module):
# remove zeros from calendar since they are not required
calendar_flat = [num for num in calendar_flat if num != 0]
# ensure all numbers have the same size
fontsize_numbers = int(min(icon_width, icon_height) * 0.5)
number_font = ImageFont.truetype(self.font.path, fontsize_numbers)
# Add the numbers on the correct positions
for number in calendar_flat:
if number != int(now.day):
@@ -215,9 +218,7 @@ class Calendar(inkycal_module):
grid[number],
(icon_width, icon_height),
str(number),
font=self.num_font,
fill_height=0.5,
fill_width=0.5,
font=number_font,
)
# Draw a red/black circle with the current day of month in white
@@ -262,10 +263,10 @@ class Calendar(inkycal_module):
from inkycal.modules.ical_parser import iCalendar
# find out how many lines can fit at max in the event section
line_spacing = 0
max_event_lines = events_height // (
self.font.getsize('hg')[1] + line_spacing
)
line_spacing = 2
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
max_event_lines = events_height // (line_height + line_spacing)
# generate list of coordinates for each line
events_offset = im_height - events_height
@@ -293,14 +294,27 @@ class Calendar(inkycal_module):
month_events = parser.get_events(month_start, month_end, self.timezone)
parser.sort()
self.month_events = month_events
# Initialize days_with_events as an empty list
days_with_events = []
# find out on which days of this month events are taking place
days_with_events = [
int(events['begin'].format('D')) for events in month_events
]
# Handle multi-day events by adding all days between start and end
for event in month_events:
start_date = event['begin'].date()
end_date = event['end'].date()
# Convert start and end dates to arrow objects with timezone
start = arrow.get(event['begin'].date(), tzinfo=self.timezone)
end = arrow.get(event['end'].date(), tzinfo=self.timezone)
# Use arrow's range function for generating dates
for day in arrow.Arrow.range('day', start, end):
day_num = int(day.format('D')) # get day number using arrow's format method
if day_num not in days_with_events:
days_with_events.append(day_num)
# remove duplicates (more than one event in a single day)
list(set(days_with_events)).sort()
days_with_events = sorted(set(days_with_events))
self._days_with_events = days_with_events
# Draw a border with specified parameters around days with events
@@ -329,31 +343,18 @@ class Calendar(inkycal_module):
# Find out how much space (width) the date format requires
lang = self.language
date_width = int(
max(
(
self.font.getsize(
events['begin'].format(self.date_format, locale=lang)
)[0]
for events in upcoming_events
)
)
* 1.1
date_width = int(max((
self.font.getlength(events['begin'].format(self.date_format, locale=lang))
for events in upcoming_events))* 1.1
)
time_width = int(
max(
(
self.font.getsize(
events['begin'].format(self.time_format, locale=lang)
)[0]
for events in upcoming_events
)
)
* 1.1
time_width = int(max((
self.font.getlength(events['begin'].format(self.time_format, locale=lang))
for events in upcoming_events))* 1.1
)
line_height = self.font.getsize('hg')[1] + line_spacing
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
event_width_s = im_width - date_width - time_width
event_width_l = im_width - date_width
@@ -365,7 +366,13 @@ class Calendar(inkycal_module):
cursor = 0
for event in upcoming_events:
if cursor < len(event_lines):
the_name = event['title']
event_duration = (event['end'] - event['begin']).days
if event_duration > 1:
# Format the duration using Arrow's localization
days_translation = arrow.get().shift(days=event_duration).humanize(only_distance=True, locale=lang)
the_name = f"{event['title']} ({days_translation})"
else:
the_name = event['title']
the_date = event['begin'].format(self.date_format, locale=lang)
the_time = event['begin'].format(self.time_format, locale=lang)
# logger.debug(f"name:{the_name} date:{the_date} time:{the_time}")
@@ -411,19 +418,16 @@ class Calendar(inkycal_module):
cursor += 1
else:
symbol = '- '
while self.font.getsize(symbol)[0] < im_width * 0.9:
while self.font.getlength(symbol) < im_width * 0.9:
symbol += ' -'
write(
im_black,
event_lines[0],
(im_width, self.font.getsize(symbol)[1]),
(im_width, line_height),
symbol,
font=self.font,
)
# return the images ready for the display
return im_black, im_colour
if __name__ == '__main__':
print(f'running {__name__} in standalone mode')

View File

@@ -1,5 +1,3 @@
#!python3
"""
Feeds module for InkyCal Project
Copyright by aceinnolab
@@ -91,9 +89,11 @@ class Feeds(inkycal_module):
# Set some parameters for formatting feeds
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
max_lines = (im_height // (line_height + line_spacing))
# Calculate padding from top so the lines look centralised
spacing_top = int(im_height % line_height / 2)
@@ -149,7 +149,3 @@ class Feeds(inkycal_module):
# return images
return im_black, im_colour
if __name__ == '__main__':
print(f'running {__name__} in standalone/debug mode')

View File

@@ -1,5 +1,3 @@
#!python3
"""
iCanHazDadJoke module for InkyCal Project
Special thanks to Erik Fredericks (@efredericks) for the template!
@@ -54,10 +52,11 @@ class Jokes(inkycal_module):
raise NetworkNotReachableError
# Set some parameters for formatting feeds
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
line_spacing = 5
text_bbox = self.font.getbbox("hg")
line_height = text_bbox[3] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
max_lines = (im_height // (line_height + line_spacing))
logger.debug(f"max_lines: {max_lines}")
@@ -97,7 +96,3 @@ class Jokes(inkycal_module):
# Return images for black and colour channels
return im_black, im_colour
if __name__ == '__main__':
print(f'running {__name__} in standalone/debug mode')

View File

@@ -1,4 +1,3 @@
#!python3
"""
Stocks Module for Inkycal Project
@@ -10,26 +9,18 @@ Version 0.1: Migration to Inkycal 2.0.0b
by https://github.com/worstface
"""
import os
import logging
from inkycal.modules.template import inkycal_module
from inkycal.custom import write, internet_available
import os
from PIL import Image
from matplotlib import pyplot
try:
import yfinance as yf
except ImportError:
print('yfinance is not installed! Please install with:')
print('pip3 install yfinance')
from inkycal.custom import write, internet_available
from inkycal.modules.template import inkycal_module
try:
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
except ImportError:
print('matplotlib is not installed! Please install with:')
print('pip3 install matplotlib')
import yfinance as yf
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
logger = logging.getLogger(__name__)
@@ -79,26 +70,24 @@ class Stocks(inkycal_module):
im_colour = Image.new('RGB', size=im_size, color='white')
# Create tmp path
tmpPath = '/tmp/inkycal_stocks/'
tmpPath = 'temp/'
try:
if not os.path.exists(tmpPath):
os.mkdir(tmpPath)
print(f"Successfully created tmp directory {tmpPath} ")
except OSError:
print(f"Creation of tmp directory {tmpPath} failed")
if not os.path.exists(tmpPath):
print(f"Creating tmp directory {tmpPath}")
os.mkdir(tmpPath)
# Check if internet is available
if internet_available() == True:
if internet_available():
logger.info('Connection test passed')
else:
raise Exception('Network could not be reached :/')
# Set some parameters for formatting feeds
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
text_bbox = self.font.getbbox("hg")
line_height = text_bbox[3] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
max_lines = (im_height // (line_height + line_spacing))
logger.debug(f"max_lines: {max_lines}")
@@ -211,7 +200,7 @@ class Stocks(inkycal_module):
else:
parsed_tickers_colour.append("")
if (_ < len(tickerCount)):
if _ < len(tickerCount):
parsed_tickers.append("")
parsed_tickers_colour.append("")
@@ -232,9 +221,10 @@ class Stocks(inkycal_module):
logger.info(f'chartSpace is...{im_width} {im_height}')
logger.info(f'open chart ...{chartPath}')
chartImage = Image.open(chartPath)
chartImage.thumbnail((im_width / 4, line_height * 4), Image.BICUBIC)
chartImage.thumbnail((int(im_width / 4), int(line_height * 4)), Image.BICUBIC)
pyplot.close()
chartPasteX = im_width - (chartImage.width)
chartPasteX = im_width - chartImage.width
chartPasteY = line_height * 5 * _
logger.info(f'pasting chart image with index {_} to...{chartPasteX} {chartPasteY}')
@@ -265,6 +255,3 @@ class Stocks(inkycal_module):
# Save image of black and colour channel in image-folder
return im_black, im_colour
if __name__ == '__main__':
print('running module in standalone/debug mode')

View File

@@ -1,4 +1,3 @@
#!python3
"""
Textfile module for InkyCal Project
@@ -7,44 +6,29 @@ If the content is too long, it will be truncated from the back until it fits
Copyright by aceinnolab
"""
from inkycal.modules.template import inkycal_module
from inkycal.custom import *
from urllib.request import urlopen
from inkycal.custom import *
from inkycal.modules.template import inkycal_module
logger = logging.getLogger(__name__)
class TextToDisplay(inkycal_module):
"""TextToDisplay module
"""TextToDisplay module - Display text from a local file on the display
"""
name = "Text module - Display text from a local file on the display"
requires = {
"filepath": {
"label": "Please enter a filepath or URL pointing to a .txt file",
},
}
def __init__(self, config):
"""Initialize inkycal_textfile_to_display module"""
super().__init__(config)
config = config['config']
# Check if all required parameters are present
for param in self.requires:
if param not in config:
raise Exception(f'config is missing {param}')
# required parameters
self.filepath = config["filepath"]
self.make_request = True if self.filepath.startswith("https://") else False
# give an OK message
print(f'{__name__} loaded')
@@ -66,17 +50,12 @@ class TextToDisplay(inkycal_module):
im_black = Image.new('RGB', size=im_size, color='white')
im_colour = Image.new('RGB', size=im_size, color='white')
# Check if internet is available
if internet_available():
logger.info('Connection test passed')
else:
raise NetworkNotReachableError
# Set some parameters for formatting feeds
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
line_spacing = 4
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
max_lines = im_height // line_height
# Calculate padding from top so the lines look centralised
spacing_top = int(im_height % line_height / 2)
@@ -87,6 +66,11 @@ class TextToDisplay(inkycal_module):
if self.make_request:
logger.info("Detected http path, making request")
# Check if internet is available
if internet_available():
logger.info('Connection test passed')
else:
raise NetworkNotReachableError
file_content = urlopen(self.filepath).read().decode('utf-8')
else:
# Create list containing all lines
@@ -111,7 +95,3 @@ class TextToDisplay(inkycal_module):
# return images
return im_black, im_colour
if __name__ == '__main__':
print(f'running {__name__} in standalone/debug mode')

View File

@@ -1,5 +1,3 @@
#!python3
"""
Inkycal Todoist Module
Copyright by aceinnolab
@@ -86,9 +84,10 @@ class Todoist(inkycal_module):
# Set some parameters for formatting todos
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
max_lines = im_height // line_height
# Calculate padding from top so the lines look centralised
spacing_top = int(im_height % line_height / 2)
@@ -197,7 +196,3 @@ class Todoist(inkycal_module):
# return the images ready for the display
return im_black, im_colour
if __name__ == '__main__':
print(f'running {__name__} in standalone/debug mode')

View File

@@ -12,7 +12,7 @@ import math
import decimal
import arrow
from pyowm.owm import OWM
from inkycal.custom import OpenWeatherMap
logger = logging.getLogger(__name__)
@@ -95,7 +95,7 @@ class Weather(inkycal_module):
self.use_beaufort = config['use_beaufort']
# additional configuration
self.owm = OWM(self.api_key).weather_manager()
self.owm = OpenWeatherMap(api_key=self.api_key, city_id=self.location, units=config['units'])
self.timezone = get_system_tz()
self.locale = config['language']
self.weatherfont = ImageFont.truetype(
@@ -104,6 +104,42 @@ class Weather(inkycal_module):
# give an OK message
print(f"{__name__} loaded")
@staticmethod
def mps_to_beaufort(meters_per_second:float) -> int:
"""Map meters per second to the beaufort scale.
Args:
meters_per_second:
float representing meters per seconds
Returns:
an integer of the beaufort scale mapping the input
"""
thresholds = [0.3, 1.6, 3.4, 5.5, 8.0, 10.8, 13.9, 17.2, 20.7, 24.5, 28.4]
return next((i for i, threshold in enumerate(thresholds) if meters_per_second < threshold), 11)
@staticmethod
def mps_to_mph(meters_per_second:float) -> float:
"""Map meters per second to miles per hour, rounded to one decimal place.
Args:
meters_per_second:
float representing meters per seconds.
Returns:
float representing the input value in miles per hour.
"""
# 1 m/s is approximately equal to 2.23694 mph
miles_per_hour = meters_per_second * 2.23694
return round(miles_per_hour, 1)
@staticmethod
def celsius_to_fahrenheit(celsius:int or float):
"""Converts the given temperate from degrees Celsius to Fahrenheit."""
fahrenheit = (celsius * 9 / 5) + 32
return fahrenheit
def generate_image(self):
"""Generate image for this module"""
@@ -124,7 +160,11 @@ class Weather(inkycal_module):
raise NetworkNotReachableError
def get_moon_phase():
"""Calculate the current (approximate) moon phase"""
"""Calculate the current (approximate) moon phase
Returns:
The corresponding moonphase-icon.
"""
dec = decimal.Decimal
diff = now - arrow.get(2001, 1, 1)
@@ -154,7 +194,7 @@ class Weather(inkycal_module):
return answer
# Lookup-table for weather icons and weather codes
weathericons = {
weather_icons = {
'01d': '\uf00d',
'02d': '\uf002',
'03d': '\uf013',
@@ -227,26 +267,26 @@ class Weather(inkycal_module):
# Increase fontsize to fit specified height and width of text box
size = 8
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getsize(text)
text_width, text_height = font.getbbox(text)[2:]
while (text_width < int(box_width * 0.9) and
text_height < int(box_height * 0.9)):
size += 1
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getsize(text)
text_width, text_height = font.getbbox(text)[2:]
text_width, text_height = font.getsize(text)
text_width, text_height = font.getbbox(text)[2:]
# Align text to desired position
x = int((box_width / 2) - (text_width / 2))
y = int((box_height / 2) - (text_height / 2) - (icon_size_correction[icon] * size) / 2)
y = int((box_height / 2) - (text_height / 2))
# Draw the text in the text-box
draw = ImageDraw.Draw(image)
space = Image.new('RGBA', (box_width, box_height))
ImageDraw.Draw(space).text((x, y), text, fill='black', font=font)
if rotation != None:
if rotation:
space.rotate(rotation, expand=True)
# Update only region with text (add text with transparent background)
@@ -350,14 +390,9 @@ class Weather(inkycal_module):
temp_fc4 = (col7, row3)
# Create current-weather and weather-forecast objects
if self.location.isdigit():
logging.debug('looking up location by ID')
weather = self.owm.weather_at_id(int(self.location)).weather
forecast = self.owm.forecast_at_id(int(self.location), '3h')
else:
logging.debug('looking up location by string')
weather = self.owm.weather_at_place(self.location).weather
forecast = self.owm.forecast_at_place(self.location, '3h')
logging.debug('looking up location by ID')
weather = self.owm.get_current_weather()
forecast = self.owm.get_weather_forecast()
# Set decimals
dec_temp = None if self.round_temperature == True else 1
@@ -369,12 +404,14 @@ class Weather(inkycal_module):
elif self.units == 'imperial':
temp_unit = 'fahrenheit'
logging.debug(f'temperature unit: {temp_unit}')
logging.debug(f'temperature unit: {self.units}')
logging.debug(f'decimals temperature: {dec_temp} | decimals wind: {dec_wind}')
# Get current time
now = arrow.utcnow()
fc_data = {}
if self.forecast_interval == 'hourly':
logger.debug("getting hourly forecasts")
@@ -386,21 +423,22 @@ class Weather(inkycal_module):
else:
hour_gap = 3
# Create timings for hourly forcasts
# Create timings for hourly forecasts
forecast_timings = [now.shift(hours=+ hour_gap + _).floor('hour')
for _ in range(0, 12, 3)]
# Create forecast objects for given timings
forecasts = [forecast.get_weather_at(forecast_time.datetime) for
forecast_time in forecast_timings]
forecasts = [_ for _ in forecast if arrow.get(_["dt"]) in forecast_timings]
# Add forecast-data to fc_data dictionary
fc_data = {}
for forecast in forecasts:
temp = '{}°'.format(round(
forecast.temperature(unit=temp_unit)['temp'], ndigits=dec_temp))
if self.units == "metric":
temp = f"{round(weather['main']['temp'], ndigits=dec_temp)}°C"
else:
temp = f"{round(self.celsius_to_fahrenheit(weather['weather']['main']['temp']), ndigits=dec_temp)}°F"
icon = forecast.weather_icon_name
icon = forecast["weather"][0]["icon"]
fc_data['fc' + str(forecasts.index(forecast) + 1)] = {
'temp': temp,
'icon': icon,
@@ -412,38 +450,35 @@ class Weather(inkycal_module):
logger.debug("getting daily forecasts")
def calculate_forecast(days_from_today):
def calculate_forecast(days_from_today) -> dict:
"""Get temperature range and most frequent icon code for forecast
days_from_today should be int from 1-4: e.g. 2 -> 2 days from today
"""
# Create a list containing time-objects for every 3rd hour of the day
time_range = list(arrow.Arrow.range('hour',
now.shift(days=days_from_today).floor('day'),
now.shift(days=days_from_today).ceil('day')
))[::3]
time_range = list(
arrow.Arrow.range('hour',
now.shift(days=days_from_today).floor('day'),now.shift(days=days_from_today).ceil('day')
))[::3]
# Get forecasts for each time-object
forecasts = [forecast.get_weather_at(_.datetime) for _ in time_range]
forecasts = [_ for _ in forecast if arrow.get(_["dt"]) in time_range]
# Get all temperatures for this day
daily_temp = [round(_.temperature(unit=temp_unit)['temp'],
ndigits=dec_temp) for _ in forecasts]
daily_temp = [round(_["main"]["temp"]) for _ in forecasts]
# Calculate min. and max. temp for this day
temp_range = f'{max(daily_temp)}°/{min(daily_temp)}°'
temp_range = f'{min(daily_temp)}°/{max(daily_temp)}°'
# Get all weather icon codes for this day
daily_icons = [_.weather_icon_name for _ in forecasts]
daily_icons = [_["weather"][0]["icon"] for _ in forecasts]
# Find most common element from all weather icon codes
status = max(set(daily_icons), key=daily_icons.count)
weekday = now.shift(days=days_from_today).format('ddd', locale=
self.locale)
weekday = now.shift(days=days_from_today).format('ddd', locale=self.locale)
return {'temp': temp_range, 'icon': status, 'stamp': weekday}
forecasts = [calculate_forecast(days) for days in range(1, 5)]
fc_data = {}
for forecast in forecasts:
fc_data['fc' + str(forecasts.index(forecast) + 1)] = {
'temp': forecast['temp'],
@@ -455,13 +490,15 @@ class Weather(inkycal_module):
logger.debug((key, val))
# Get some current weather details
temperature = '{}°'.format(round(
weather.temperature(unit=temp_unit)['temp'], ndigits=dec_temp))
if dec_temp != 0:
temperature = f"{round(weather['main']['temp'])}°"
else:
temperature = f"{round(weather['main']['temp'],ndigits=dec_temp)}°"
weather_icon = weather.weather_icon_name
humidity = str(weather.humidity)
sunrise_raw = arrow.get(weather.sunrise_time()).to(self.timezone)
sunset_raw = arrow.get(weather.sunset_time()).to(self.timezone)
weather_icon = weather["weather"][0]["icon"]
humidity = str(weather["main"]["humidity"])
sunrise_raw = arrow.get(weather["sys"]["sunrise"]).to(self.timezone)
sunset_raw = arrow.get(weather["sys"]["sunset"]).to(self.timezone)
logger.debug(f'weather_icon: {weather_icon}')
@@ -469,33 +506,29 @@ class Weather(inkycal_module):
logger.debug('using 12 hour format for sunrise/sunset')
sunrise = sunrise_raw.format('h:mm a')
sunset = sunset_raw.format('h:mm a')
elif self.hour_format == 24:
else:
# 24 hours format
logger.debug('using 24 hour format for sunrise/sunset')
sunrise = sunrise_raw.format('H:mm')
sunset = sunset_raw.format('H:mm')
# Format the windspeed to user preference
# Format the wind-speed to user preference
if self.use_beaufort:
logger.debug("using beaufort for wind")
wind = str(weather.wind(unit='beaufort')['speed'])
wind = str(self.mps_to_beaufort(weather["wind"]["speed"]))
else:
if self.units == 'metric':
logging.debug('getting windspeed in metric unit')
wind = str(weather.wind(unit='meters_sec')['speed']) + 'm/s'
logging.debug('getting wind speed in meters per second')
wind = f"{weather['wind']['speed']} m/s"
else:
logging.debug('getting wind speed in imperial unit')
wind = f"{self.mps_to_mph(weather['wind']['speed'])} miles/h"
elif self.units == 'imperial':
logging.debug('getting windspeed in imperial unit')
wind = str(weather.wind(unit='miles_hour')['speed']) + 'miles/h'
dec = decimal.Decimal
moonphase = get_moon_phase()
moon_phase = get_moon_phase()
# Fill weather details in col 1 (current weather icon)
draw_icon(im_colour, weather_icon_pos, (col_width, im_height),
weathericons[weather_icon])
weather_icons[weather_icon])
# Fill weather details in col 2 (temp, humidity, wind)
draw_icon(im_colour, temperature_icon_pos, (icon_small, row_height),
@@ -521,7 +554,7 @@ class Weather(inkycal_module):
wind, font=self.font)
# Fill weather details in col 3 (moonphase, sunrise, sunset)
draw_icon(im_colour, moonphase_pos, (col_width, row_height), moonphase)
draw_icon(im_colour, moonphase_pos, (col_width, row_height), moon_phase)
draw_icon(im_colour, sunrise_icon_pos, (icon_small, icon_small), '\uf051')
write(im_black, sunrise_time_pos, (col_width - icon_small, row_height),
@@ -535,7 +568,7 @@ class Weather(inkycal_module):
for pos in range(1, len(fc_data) + 1):
stamp = fc_data[f'fc{pos}']['stamp']
icon = weathericons[fc_data[f'fc{pos}']['icon']]
icon = weather_icons[fc_data[f'fc{pos}']['icon']]
temp = fc_data[f'fc{pos}']['temp']
write(im_black, eval(f'stamp_fc{pos}'), (col_width, row_height),
@@ -548,7 +581,7 @@ class Weather(inkycal_module):
border_h = row3 + row_height
border_w = col_width - 3 # leave 3 pixels gap
# Add borders around each sub-section
# Add borders around each subsection
draw_border(im_black, (col1, row1), (col_width * 3 - 3, border_h),
shrinkage=(0, 0))

View File

@@ -0,0 +1,164 @@
"""
Webshot module for Inkycal
by https://github.com/worstface
"""
from htmlwebshot import WebShot
from inkycal.custom import *
from inkycal.modules.inky_image import Inkyimage as Images
from inkycal.modules.template import inkycal_module
from tests import Config
logger = logging.getLogger(__name__)
class Webshot(inkycal_module):
name = "Webshot - Displays screenshots of webpages"
# required parameters
requires = {
"url": {
"label": "Please enter the url",
},
"palette": {
"label": "Which color palette should be used for the webshots?",
"options": ["bw", "bwr", "bwy"]
}
}
optional = {
"crop_x": {
"label": "Please enter the crop x-position",
},
"crop_y": {
"label": "Please enter the crop y-position",
},
"crop_w": {
"label": "Please enter the crop width",
},
"crop_h": {
"label": "Please enter the crop height",
}
}
def __init__(self, config):
super().__init__(config)
config = config['config']
self.url = config['url']
self.palette = config['palette']
if "crop_h" in config and isinstance(config["crop_h"], str):
self.crop_h = int(config["crop_h"])
else:
self.crop_h = 2000
if "crop_w" in config and isinstance(config["crop_w"], str):
self.crop_w = int(config["crop_w"])
else:
self.crop_w = 2000
if "crop_x" in config and isinstance(config["crop_x"], str):
self.crop_x = int(config["crop_x"])
else:
self.crop_x = 0
if "crop_y" in config and isinstance(config["crop_y"], str):
self.crop_y = int(config["crop_y"])
else:
self.crop_y = 0
# give an OK message
print(f'Inkycal webshot loaded')
def generate_image(self):
"""Generate image for this module"""
# Create tmp path
tmpFolder = Config.TEMP_PATH
if not os.path.exists(tmpFolder):
print(f"Creating tmp directory {tmpFolder}")
os.mkdir(tmpFolder)
# 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('image size: {} x {} px'.format(im_width, im_height))
# Create an image for black pixels and one for coloured pixels (required)
im_black = Image.new('RGB', size=im_size, color='white')
im_colour = Image.new('RGB', size=im_size, color='white')
# Check if internet is available
if internet_available():
logger.info('Connection test passed')
else:
raise Exception('Network could not be reached :/')
logger.info(
f'preparing webshot from {self.url}... cropH{self.crop_h} cropW{self.crop_w} cropX{self.crop_x} cropY{self.crop_y}')
shot = WebShot()
shot.params = {
"--crop-x": self.crop_x,
"--crop-y": self.crop_y,
"--crop-w": self.crop_w,
"--crop-h": self.crop_h,
}
logger.info(f'getting webshot from {self.url}...')
try:
shot.create_pic(url=self.url, output=f"{tmpFolder}/webshot.png")
except:
print(traceback.format_exc())
print("If you have not already installed wkhtmltopdf, please use: sudo apt-get install wkhtmltopdf. See here for more details: https://github.com/1Danish-00/htmlwebshot/")
raise Exception('Could not get webshot :/')
logger.info(f'got webshot...')
webshotSpaceBlack = Image.new('RGBA', (im_width, im_height), (255, 255, 255, 255))
webshotSpaceColour = Image.new('RGBA', (im_width, im_height), (255, 255, 255, 255))
im = Images()
im.load(f'{tmpFolder}/webshot.png')
im.remove_alpha()
imageAspectRatio = im_width / im_height
webshotAspectRatio = im.image.width / im.image.height
if webshotAspectRatio > imageAspectRatio:
imageScale = im_width / im.image.width
else:
imageScale = im_height / im.image.height
webshotHeight = int(im.image.height * imageScale)
im.resize(width=int(im.image.width * imageScale), height=webshotHeight)
im_webshot_black, im_webshot_colour = im.to_palette(self.palette)
webshotCenterPosY = int((im_height / 2) - (im.image.height / 2))
centerPosX = int((im_width / 2) - (im.image.width / 2))
webshotSpaceBlack.paste(im_webshot_black, (centerPosX, webshotCenterPosY))
im_black.paste(webshotSpaceBlack)
webshotSpaceColour.paste(im_webshot_colour, (centerPosX, webshotCenterPosY))
im_colour.paste(webshotSpaceColour)
im.clear()
logger.info(f'added webshot image')
# Save image of black and colour channel in image-folder
return im_black, im_colour

View File

@@ -0,0 +1,202 @@
"""
Inkycal XKCD module
by https://github.com/worstface
"""
import xkcd
from inkycal.custom import *
from inkycal.modules.inky_image import Inkyimage as Images
from inkycal.modules.template import inkycal_module
logger = logging.getLogger(__name__)
class Xkcd(inkycal_module):
name = "xkcd - Displays comics from xkcd.com by Randall Munroe"
# required parameters
requires = {
"mode": {
"label": "Please select the mode",
"options": ["latest", "random"],
"default": "latest"
},
"palette": {
"label": "Which color palette should be used for the comic images?",
"options": ["bw", "bwr", "bwy"]
},
"alt": {
"label": "Would you like to add the alt text below the comic? If XKCD is not the only module you are showing, I recommend setting this to 'no'",
"options": ["yes", "no"],
"default": "no"
},
"filter": {
"label": "Would you like to add a scaling filter? If the is far too big to be shown in the space you've allotted for it, the module will try to find another image for you. This only applies in random mode. If XKCD is not the only module you are showing, I recommend setting this to 'no'.",
"options": ["yes", "no"],
"default": "no"
}
}
def __init__(self, config):
super().__init__(config)
config = config['config']
self.mode = config['mode']
self.palette = config['palette']
self.alt = config['alt']
self.scale_filter = config['filter']
# give an OK message
print(f'Inkycal XKCD loaded')
def generate_image(self):
"""Generate image for this module"""
# Create tmp path
tmpPath = f"{top_level}/temp"
if not os.path.exists(tmpPath):
os.mkdir(tmpPath)
# 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('image size: {} x {} px'.format(im_width, im_height))
# Create an image for black pixels and one for coloured pixels (required)
im_black = Image.new('RGB', size=im_size, color='white')
im_colour = Image.new('RGB', size=im_size, color='white')
# Check if internet is available
if internet_available():
logger.info('Connection test passed')
else:
raise Exception('Network could not be reached :/')
# Set some parameters for formatting feeds
line_spacing = 1
text_bbox = self.font.getbbox("hg")
line_height = text_bbox[3] + line_spacing
line_width = im_width
max_lines = im_height // (line_height + line_spacing)
logger.debug(f"max_lines: {max_lines}")
# Calculate padding from top so the lines look centralised
spacing_top = int(im_height % line_height / 2)
# Calculate line_positions
line_positions = [(0, spacing_top + _ * line_height) for _ in range(max_lines)]
logger.debug(f'line positions: {line_positions}')
logger.info(f'getting xkcd comic...')
if self.mode == 'random':
if self.scale_filter == 'no':
xkcdComic = xkcd.getRandomComic()
xkcdComic.download(output=tmpPath, outputFile='xkcdComic.png')
else:
perc = (2.1, 0.4)
url = "test variable, not a real comic"
while max(perc) > 1.75:
print("looking for another comic, old comic was: ", perc, url)
xkcdComic = xkcd.getRandomComic()
xkcdComic.download(output=tmpPath, outputFile='xkcdComic.png')
actual_size = Image.open(tmpPath + '/xkcdComic.png').size
perc = (actual_size[0] / im_width, actual_size[1] / im_height)
url = xkcdComic.getImageLink()
print("found one! perc: ", perc, url)
else:
xkcdComic = xkcd.getLatestComic()
xkcdComic.download(output=tmpPath, outputFile='xkcdComic.png')
logger.info(f'got xkcd comic...')
title_lines = []
title_lines.append(xkcdComic.getTitle())
altOffset = int(line_height * 1)
if self.alt == "yes":
alt_text = xkcdComic.getAltText() # get the alt text, too (I break it up into multiple lines later on)
# break up the alt text into lines
alt_lines = []
current_line = ""
for _ in alt_text.split(" "):
# this breaks up the alt_text into words and creates each line by adding
# one word at a time until the line is longer than the width of the module
# then it appends the line to the alt_lines array and starts testing a new line
# with the next word
text_bbox = self.font.getbbox(current_line + _ + " ")
if text_bbox[2] < im_width:
current_line = current_line + _ + " "
else:
alt_lines.append(current_line)
current_line = _ + " "
alt_lines.append(
current_line) # this adds the last line to the array (or the only line, if the alt text is really short)
altHeight = int(line_height * len(alt_lines)) + altOffset
else:
altHeight = 0 # this is added so that I don't need to add more "if alt is yes" conditionals when centering below. Now the centering code will work regardless of whether they want alttext or not
comicSpaceBlack = Image.new('RGBA', (im_width, im_height), (255, 255, 255, 255))
comicSpaceColour = Image.new('RGBA', (im_width, im_height), (255, 255, 255, 255))
im = Images()
im.load(f"{tmpPath}/xkcdComic.png")
im.remove_alpha()
imageAspectRatio = im_width / im_height
comicAspectRatio = im.image.width / im.image.height
if comicAspectRatio > imageAspectRatio:
imageScale = im_width / im.image.width
else:
imageScale = im_height / im.image.height
comicHeight = int(im.image.height * imageScale)
headerHeight = int(line_height * 3 / 2)
if comicHeight + (headerHeight + altHeight) > im_height:
comicHeight -= (headerHeight + altHeight)
im.resize(width=int(im.image.width * imageScale), height=comicHeight)
im_comic_black, im_comic_colour = im.to_palette(self.palette)
headerCenterPosY = int((im_height / 2) - ((im.image.height + headerHeight + altHeight) / 2))
comicCenterPosY = int((im_height / 2) - ((im.image.height + headerHeight + altHeight) / 2) + headerHeight)
altCenterPosY = int(
(im_height / 2) - ((im.image.height + headerHeight + altHeight) / 2) + headerHeight + im.image.height)
centerPosX = int((im_width / 2) - (im.image.width / 2))
comicSpaceBlack.paste(im_comic_black, (centerPosX, comicCenterPosY))
im_black.paste(comicSpaceBlack)
comicSpaceColour.paste(im_comic_colour, (centerPosX, comicCenterPosY))
im_colour.paste(comicSpaceColour)
im.clear()
logger.info(f'added comic image')
# Write the title on the black image
write(im_black, (0, headerCenterPosY), (line_width, line_height),
title_lines[0], font=self.font, alignment='center')
if self.alt == "yes":
# write alt_text
for _ in range(len(alt_lines)):
write(im_black, (0, altCenterPosY + _ * line_height + altOffset), (line_width, line_height),
alt_lines[_], font=self.font, alignment='left')
# Save image of black and colour channel in image-folder
return im_black, im_colour