done for the left section

This commit is contained in:
2025-11-30 18:01:30 +01:00
parent a00892586b
commit 70c36111a6
6 changed files with 420 additions and 1 deletions

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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"

View File

@@ -18,7 +18,7 @@
}, },
{ {
"position": 2, "position": 2,
"name": "Calendar", "name": "Today",
"config": { "config": {
"size": [ "size": [
528, 528,

View 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()