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