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_fullweather import Fullweather
|
||||||
from .inkycal_tindie import Tindie
|
from .inkycal_tindie import Tindie
|
||||||
from .inkycal_vikunja import Vikunja
|
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 ImageFont
|
||||||
from PIL import ImageOps
|
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 icons.weather_icons.weather_icons import get_weather_icon
|
||||||
from inkycal.custom.functions import fonts
|
from inkycal.custom.functions import fonts
|
||||||
from inkycal.custom.functions import get_system_tz
|
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_API_KEY = get("TINDIE_API_KEY")
|
||||||
TINDIE_USERNAME = get("TINDIE_USERNAME")
|
TINDIE_USERNAME = get("TINDIE_USERNAME")
|
||||||
|
|
||||||
|
OUTPUT_DIR = f"{basedir}/../image_folder"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"position": 2,
|
"position": 2,
|
||||||
"name": "Calendar",
|
"name": "Today",
|
||||||
"config": {
|
"config": {
|
||||||
"size": [
|
"size": [
|
||||||
528,
|
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