done for the left section
This commit is contained in:
@@ -13,3 +13,4 @@ from .inkycal_xkcd import Xkcd
|
||||
from .inkycal_fullweather import Fullweather
|
||||
from .inkycal_tindie import Tindie
|
||||
from .inkycal_vikunja import Vikunja
|
||||
from .inkycal_today import Today
|
||||
|
||||
@@ -19,6 +19,11 @@ from PIL import ImageDraw
|
||||
from PIL import ImageFont
|
||||
from PIL import ImageOps
|
||||
|
||||
import sys
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
if project_root not in sys.path:
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
from icons.weather_icons.weather_icons import get_weather_icon
|
||||
from inkycal.custom.functions import fonts
|
||||
from inkycal.custom.functions import get_system_tz
|
||||
|
||||
328
inkycal/modules/inkycal_today.py
Normal file
328
inkycal/modules/inkycal_today.py
Normal file
@@ -0,0 +1,328 @@
|
||||
"""
|
||||
Inkycal Calendar Module
|
||||
Copyright by aceinnolab
|
||||
"""
|
||||
|
||||
# pylint: disable=logging-fstring-interpolation
|
||||
|
||||
import calendar as cal
|
||||
|
||||
from inkycal.custom import *
|
||||
from inkycal.modules.template import inkycal_module
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_ip_address():
|
||||
"""Get public IP address from external service."""
|
||||
try:
|
||||
# 方法1: 使用 ipify.org
|
||||
response = requests.get('https://api.ipify.org?format=json', timeout=5)
|
||||
return response.json()['ip']
|
||||
except Exception:
|
||||
try:
|
||||
# 方法2: 使用 icanhazip.com (备用)
|
||||
response = requests.get('https://icanhazip.com', timeout=5)
|
||||
return response.text.strip()
|
||||
except Exception:
|
||||
try:
|
||||
# 方法3: 使用 ifconfig.me (备用)
|
||||
response = requests.get('https://ifconfig.me/ip', timeout=5)
|
||||
return response.text.strip()
|
||||
except Exception:
|
||||
return "N/A"
|
||||
|
||||
class Today(inkycal_module):
|
||||
"""today class
|
||||
Show today's date and events from given iCalendars
|
||||
"""
|
||||
|
||||
name = "Today - Show today's date and events from iCalendars"
|
||||
|
||||
optional = {
|
||||
"week_starts_on": {
|
||||
"label": "When does your week start? (default=Monday)",
|
||||
"options": ["Monday", "Sunday"],
|
||||
"default": "Monday",
|
||||
},
|
||||
"show_events": {
|
||||
"label": "Show parsed events? (default = True)",
|
||||
"options": [True, False],
|
||||
"default": True,
|
||||
},
|
||||
"ical_urls": {
|
||||
"label": "iCalendar URL/s, separate multiple ones with a comma",
|
||||
},
|
||||
"ical_files": {
|
||||
"label": "iCalendar filepaths, separated with a comma",
|
||||
},
|
||||
"date_format": {
|
||||
"label": "Use an arrow-supported token for custom date formatting "
|
||||
+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM",
|
||||
"default": "D MMM",
|
||||
},
|
||||
"time_format": {
|
||||
"label": "Use an arrow-supported token for custom time formatting "
|
||||
+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm",
|
||||
"default": "HH:mm",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize inkycal_calendar module"""
|
||||
|
||||
super().__init__(config)
|
||||
config = config['config']
|
||||
|
||||
self.ical = None
|
||||
self.month_events = None
|
||||
self._upcoming_events = None
|
||||
self._days_with_events = None
|
||||
|
||||
# optional parameters
|
||||
self.week_start = config['week_starts_on']
|
||||
self.show_events = config['show_events']
|
||||
self.date_format = config["date_format"]
|
||||
self.time_format = config['time_format']
|
||||
self.language = config['language']
|
||||
|
||||
if config['ical_urls'] and isinstance(config['ical_urls'], str):
|
||||
self.ical_urls = config['ical_urls'].split(',')
|
||||
else:
|
||||
self.ical_urls = config['ical_urls']
|
||||
|
||||
if config['ical_files'] and isinstance(config['ical_files'], str):
|
||||
self.ical_files = config['ical_files'].split(',')
|
||||
else:
|
||||
self.ical_files = config['ical_files']
|
||||
|
||||
# additional configuration
|
||||
self.timezone = get_system_tz()
|
||||
self.num_font = ImageFont.truetype(
|
||||
fonts['NotoSans-SemiCondensed'], size=self.fontsize
|
||||
)
|
||||
|
||||
# give an OK message
|
||||
logger.debug(f'{__name__} loaded')
|
||||
|
||||
@staticmethod
|
||||
def flatten(values):
|
||||
"""Flatten the values."""
|
||||
return [x for y in values for x in y]
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate the image for today's date and events. """
|
||||
|
||||
# ****************************************************************************************************************
|
||||
# Create base image
|
||||
|
||||
# Define new image size with respect to padding
|
||||
im_width = self.width - 2 * self.padding_left
|
||||
im_height = self.height - 2 * self.padding_top
|
||||
im_size = (im_width, im_height)
|
||||
|
||||
event_height = 0
|
||||
|
||||
logger.debug(f'Generating Today module image of size {im_size}')
|
||||
|
||||
# Create an iamge for black and colour Inky displays
|
||||
im_black = Image.new('RGB', im_size, color='white')
|
||||
im_colour = Image.new('RGB', im_size, color='white')
|
||||
|
||||
# Split the image into two sections: date section and events section
|
||||
left_section_width = int(im_width * 0.2)
|
||||
right_section_width = im_width - left_section_width
|
||||
|
||||
# 5% bottom space will be reserved for show the day progress bar
|
||||
left_section = (0, 0, left_section_width, im_height - int(im_height * 0.05))
|
||||
right_section = (left_section_width, 0, im_width, im_height - int(im_height * 0.05))
|
||||
|
||||
section_height = left_section[3]
|
||||
|
||||
|
||||
# ****************************************************************************************************************
|
||||
# Edit left section - show today's date
|
||||
now = arrow.now(tz=self.timezone)
|
||||
month_height = int(im_height * 0.15)
|
||||
month_font = ImageFont.truetype(
|
||||
fonts['NotoSans-SemiCondensed'], size=int(self.fontsize * 1.5)
|
||||
)
|
||||
write(
|
||||
im_black,
|
||||
(0, 0),
|
||||
(left_section_width, month_height),
|
||||
now.format('MMMM', locale=self.language),
|
||||
font=month_font,
|
||||
autofit=False
|
||||
)
|
||||
|
||||
date_height = int(im_height * 0.5)
|
||||
date_y = month_height
|
||||
large_font = ImageFont.truetype(
|
||||
fonts['NotoSans-SemiCondensed'], size=int(self.fontsize * 4)
|
||||
)
|
||||
date_time = arrow.now()
|
||||
day = date_time.day
|
||||
print(str(day))
|
||||
write(
|
||||
im_colour,
|
||||
(0, date_y),
|
||||
(left_section_width, date_height),
|
||||
str(day),
|
||||
font=large_font,
|
||||
autofit=False
|
||||
)
|
||||
|
||||
weekday_y = month_height + date_height
|
||||
weekday_height = int(im_height * 0.15)
|
||||
weekday_font = ImageFont.truetype(
|
||||
fonts['NotoSans-SemiCondensed'], size=int(self.fontsize * 2)
|
||||
)
|
||||
write(
|
||||
im_black,
|
||||
(0, weekday_y),
|
||||
(left_section_width, weekday_height),
|
||||
now.format('dddd', locale=self.language),
|
||||
font=weekday_font,
|
||||
autofit=False
|
||||
)
|
||||
|
||||
# show IP address at the bottom left
|
||||
ip_y = weekday_y + weekday_height
|
||||
ip_height = im_height - ip_y - 5
|
||||
ip_address = get_ip_address()
|
||||
write(
|
||||
im_black,
|
||||
(0, ip_y),
|
||||
(left_section_width, ip_height),
|
||||
ip_address,
|
||||
font=self.font,
|
||||
alignment='center',
|
||||
autofit=True
|
||||
)
|
||||
|
||||
# ****************************************************************************************************************
|
||||
# Draw a dash line to separate left and right sections
|
||||
for _y in range(0, section_height, 8):
|
||||
|
||||
ImageDraw.Draw(im_black).line(
|
||||
[(left_section_width, _y), (left_section_width, _y + 4)],
|
||||
fill='black',
|
||||
width=2,
|
||||
)
|
||||
|
||||
# ****************************************************************************************************************
|
||||
# Edit right section - show today's events
|
||||
if self.show_events:
|
||||
# 导入日历解析器
|
||||
from inkycal.modules.ical_parser import iCalendar
|
||||
|
||||
parser = iCalendar()
|
||||
|
||||
if self.ical_urls:
|
||||
parser.load_url(self.ical_urls)
|
||||
if self.ical_files:
|
||||
parser.load_from_file(self.ical_files)
|
||||
|
||||
# 获取今天的事件
|
||||
today_start = now.floor('day')
|
||||
today_end = now.ceil('day')
|
||||
upcoming_events = parser.get_events(today_start, today_end, self.timezone)
|
||||
|
||||
# 计算右侧可用空间
|
||||
right_x = left_section_width + 5 # 留5px边距
|
||||
right_usable_width = right_section_width - 10 # 左右各留5px
|
||||
|
||||
# 计算行高
|
||||
line_spacing = 2
|
||||
text_bbox = self.font.getbbox("hg")
|
||||
line_height = text_bbox[3] + line_spacing
|
||||
max_lines = im_height // line_height
|
||||
|
||||
if upcoming_events:
|
||||
# 显示事件
|
||||
cursor = 0
|
||||
for event in upcoming_events[:max_lines]:
|
||||
if cursor >= max_lines:
|
||||
break
|
||||
|
||||
y_pos = cursor * line_height
|
||||
|
||||
# 显示时间
|
||||
time_str = event['begin'].format(self.time_format, locale=self.language)
|
||||
time_width = int(self.font.getlength(time_str) * 1.1)
|
||||
|
||||
write(
|
||||
im_black,
|
||||
(right_x, y_pos),
|
||||
(time_width, line_height),
|
||||
time_str,
|
||||
font=self.font,
|
||||
alignment='left'
|
||||
)
|
||||
|
||||
# 显示事件标题
|
||||
event_x = right_x + time_width + 5
|
||||
event_width = right_usable_width - time_width - 5
|
||||
|
||||
write(
|
||||
im_black,
|
||||
(event_x, y_pos),
|
||||
(event_width, line_height),
|
||||
event['title'],
|
||||
font=self.font,
|
||||
alignment='left'
|
||||
)
|
||||
|
||||
cursor += 1
|
||||
else:
|
||||
# 没有事件时显示提示
|
||||
write(
|
||||
im_black,
|
||||
(right_x, int(im_height / 2)),
|
||||
(right_usable_width, line_height),
|
||||
"No events today",
|
||||
font=self.font,
|
||||
alignment='center'
|
||||
)
|
||||
|
||||
# ****************************************************************************************************************
|
||||
# Draw progress bar at the bottom (24 segments for 24 hours)
|
||||
progress_bar_height = int(im_height * 0.05)
|
||||
progress_bar_y = im_height - progress_bar_height
|
||||
|
||||
# 计算当前小时进度
|
||||
current_hour = now.hour
|
||||
current_minute = now.minute
|
||||
current_progress = current_hour + (current_minute / 60.0) # 0-24 的浮点数
|
||||
|
||||
# 绘制24个格子
|
||||
num_segments = 24
|
||||
segment_spacing = 2 # 格子之间的间距
|
||||
total_spacing = segment_spacing * (num_segments - 1)
|
||||
segment_width = (im_width - total_spacing) / num_segments
|
||||
|
||||
draw = ImageDraw.Draw(im_black)
|
||||
|
||||
for i in range(num_segments):
|
||||
# 计算每个格子的位置
|
||||
x_start = int(i * (segment_width + segment_spacing))
|
||||
x_end = int(x_start + segment_width)
|
||||
|
||||
# 判断该格子是否已完成
|
||||
if i < current_progress:
|
||||
# 已完成的格子填充黑色(在 im_colour 上会显示为红色)
|
||||
draw.rectangle(
|
||||
[(x_start, progress_bar_y), (x_end, im_height)],
|
||||
fill='black',
|
||||
outline='black'
|
||||
)
|
||||
else:
|
||||
# 未完成的格子只画边框(在 im_black 上画)
|
||||
ImageDraw.Draw(im_black).rectangle(
|
||||
[(x_start, progress_bar_y), (x_end, im_height)],
|
||||
fill='white',
|
||||
outline='black',
|
||||
width=1
|
||||
)
|
||||
|
||||
return im_black, im_colour
|
||||
@@ -36,6 +36,8 @@ class Config:
|
||||
TINDIE_API_KEY = get("TINDIE_API_KEY")
|
||||
TINDIE_USERNAME = get("TINDIE_USERNAME")
|
||||
|
||||
OUTPUT_DIR = f"{basedir}/../image_folder"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"name": "Calendar",
|
||||
"name": "Today",
|
||||
"config": {
|
||||
"size": [
|
||||
528,
|
||||
|
||||
83
tests/test_inkycal_today.py
Normal file
83
tests/test_inkycal_today.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
inkycal_today unittest
|
||||
"""
|
||||
import logging
|
||||
import unittest
|
||||
|
||||
from inkycal.modules import Today
|
||||
from inkycal.modules.inky_image import Inkyimage, image_to_palette
|
||||
from tests import Config
|
||||
|
||||
merge = Inkyimage.merge
|
||||
|
||||
sample_url = Config.SAMPLE_ICAL_URL
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
# /workspaces/Inkycal/venv/bin/python3 -m pytest tests/test_inkycal_today.py::TestToday::test_generate_image -v -s
|
||||
tests = [
|
||||
{
|
||||
"name": "Today",
|
||||
"config": {
|
||||
"size": [528, 343],
|
||||
"week_starts_on": "Monday",
|
||||
"show_events": True,
|
||||
"ical_urls": sample_url,
|
||||
"ical_files": None,
|
||||
"date_format": "D MMM", "time_format": "HH:mm",
|
||||
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class TestToday(unittest.TestCase):
|
||||
|
||||
def test_generate_image(self):
|
||||
output_dir = Config.OUTPUT_DIR
|
||||
import os
|
||||
from PIL import Image, ImageDraw
|
||||
import numpy as np
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
test_num = 0
|
||||
for test in tests:
|
||||
print(f'test {tests.index(test) + 1} generating image..', end="")
|
||||
module = Today(test)
|
||||
im_black, im_colour = module.generate_image()
|
||||
print('OK')
|
||||
|
||||
# 创建最终的三色图像
|
||||
# 在 E-Paper 上: im_black 的黑色像素显示黑色, im_colour 的黑色像素显示红色
|
||||
result = Image.new('RGB', im_black.size, 'white')
|
||||
result_array = np.array(result)
|
||||
black_array = np.array(im_black)
|
||||
colour_array = np.array(im_colour)
|
||||
|
||||
# 使用阈值处理抗锯齿:灰度值 < 128 的视为"黑色"
|
||||
# im_black 上的深色区域 -> 黑色 (0, 0, 0)
|
||||
black_gray = black_array[:,:,0] # 取灰度值(RGB相同)
|
||||
black_mask = black_gray < 128
|
||||
result_array[black_mask] = [0, 0, 0]
|
||||
|
||||
# im_colour 上的深色区域 -> 红色 (255, 0, 0)
|
||||
colour_gray = colour_array[:,:,0]
|
||||
colour_mask = colour_gray < 128
|
||||
result_array[colour_mask] = [255, 0, 0]
|
||||
|
||||
# 保存最终图像
|
||||
final_image = Image.fromarray(result_array)
|
||||
output_path = os.path.join(output_dir, f"today_test_{test_num}.png")
|
||||
final_image.save(output_path)
|
||||
print(f' -> Saved to {output_path}')
|
||||
|
||||
# 统计颜色
|
||||
red_count = np.sum(colour_mask)
|
||||
black_count = np.sum(black_mask)
|
||||
print(f' 🔴 Red pixels: {red_count}, ⚫ Black pixels: {black_count}')
|
||||
|
||||
test_num += 1
|
||||
|
||||
if Config.USE_PREVIEW:
|
||||
final_image.show()
|
||||
Reference in New Issue
Block a user