0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python 3】CalDAVを用いたスケジュール通知の実装

Last updated at Posted at 2025-02-01

はじめに

コーディングをしていると、スケジュールのことが気になってしまうことが多いため、スケジュールの通知機能が必要になった。

はまりどころ

CalDAVを使用してカレンダーから予定を取得する際には、タイムゾーンに注意が必要である。
なぜなら、CalDAVから送られてくる日時は、サービスごとにタイムゾーンが異なると思われるためである。
例えば、DTSTARTDTSTAMPの末尾にZが付いている場合はUTCであることを示す。

工夫

  1. カレンダーの取得回数の制限:
    CalDAVサーバに負担をかけないように配慮するため、カレンダーの取得頻度を制限することが重要である。
    具体的には、以下のような工夫を行った。

    • 12時間ごとの取得:
      カレンダーの予定を12時間に一度取得するように設定した。
      これにより、サーバへのリクエスト回数を減らし、サーバの負荷を軽減することができる。
  2. リマインダー機能の実装:
    約15分前、約5分前、約1分前で通知を行うリマインダー機能を実装した。
    これにより、予定を見逃すことを防ぎ、事前に準備をする時間を確保できるようにしている。

環境

  • Python 3.12.8
  • Windows 10 22H2

必要なライブラリ

以下のコマンドで必要なライブラリをインストールする。

py -m pip install caldav==1.4.0
py -m pip install plyer==2.1.0

サンプルコード

以下に、カレンダーから予定を取得し、トースト通知を行うサンプルコードを示す。

import time
import datetime
from caldav import DAVClient, Principal, Calendar, Sequence, CalendarObjectResource
from plyer import notification

URL: str = 'https://example.com/'
USERNAME: str = 'Hoge'
PASSWORD: str = 'Fuga'

def fetch_schedules(schedules : dict[str, datetime.datetime]) -> None:
    """
    CalDAVサーバから予定を取得する。

    Args:
        schedules (dict[str, datetime.datetime]): 予定を格納する辞書。キーは予定のタイトル、値は開始時刻。
    """
    with DAVClient(url=URL, username=USERNAME, password=PASSWORD) as client:
        principal: Principal = client.principal()
        calendars: list[Calendar] = principal.calendars()

        now_utc: datetime = datetime.datetime.now(datetime.timezone.utc)
        end_time: datetime = now_utc + datetime.timedelta(days=1)

        for calendar in calendars:
            try:
                events: Sequence[CalendarObjectResource] = calendar.date_search(start=now_utc, end=end_time, expand=True)
                for event in events:
                    d : dict[str, str] = {}
                    for event_data in event.data.splitlines():
                        pair: list[str] = event_data.split(":")
                        if len(pair) >= 2: 
                            d[pair[0]] = pair[1]
                    if 'SUMMARY' in d and 'DTSTART' in d:
                        key: str = d['SUMMARY']
                        start_time_str: str = d['DTSTART']
                        start_time: datetime = datetime.datetime.strptime(start_time_str, '%Y%m%dT%H%M%SZ')
                        start_time: datetime = start_time.replace(tzinfo=datetime.timezone.utc)
                        schedules[key] = start_time
            except Exception as e:
                notification.notify(title="カレンダー取得 失敗", message=f"カレンダーの取得に失敗しました。: {str(e)}", timeout=10)
                exit(1)

def check_notifications(schedules : dict[str, datetime.datetime]) -> None:
    """
    取得したスケジュールを確認し、通知を行う。

    Args:
        schedules (dict[str, datetime.datetime]): 予定を格納する辞書。キーは予定のタイトル、値は開始時刻。
    """
    now_utc: datetime = datetime.datetime.now(datetime.timezone.utc)

    for k, start_time in schedules.items():
        event_time: datetime = start_time

        # 各通知タイミングを計算
        fifteen_minutes_before: datetime = event_time - datetime.timedelta(minutes=15)
        fourteen_minutes_before: datetime = event_time - datetime.timedelta(minutes=14)
        five_minutes_before: datetime = event_time - datetime.timedelta(minutes=5)
        six_minutes_before: datetime = event_time - datetime.timedelta(minutes=6)
        one_minute_before: datetime = event_time - datetime.timedelta(minutes=1)

        # トーストを表示する条件
        is_fifteen_minutes_before: bool = fifteen_minutes_before >= now_utc >= fourteen_minutes_before
        is_five_minutes_before: bool = five_minutes_before >= now_utc >= six_minutes_before
        is_one_minute_before: bool = event_time >= now_utc >= one_minute_before

        if is_fifteen_minutes_before or is_five_minutes_before or is_one_minute_before:
            notification.notify(title=k, message=f"予定「{k}", timeout=10)

def main() -> None:
    schedules : dict[str, datetime.datetime] = {}
    
    while True:
        fetch_schedules(schedules) 
        
        for _ in range(12 * 60):
            check_notifications(schedules)
            time.sleep(60)

if __name__ == "__main__":
    main()

さいごに

CalDAVを使用してカレンダーから予定を取得し、指定した時間の前に通知を行う機能を実装した。
この機能により、安心してコーディングを行えるようになった。

参考文献

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?