試してみたブログ

AI関連・iPhone/Pixelなどのガジェット・音声入力・サーマルプリンタなど興味をある事をどんどん試してみた際の記録

Googleカレンダーの連絡帳を取得して定期的にサーマルプリンタから印刷する

背景・やりたいこと

  • 小学生の娘の連絡帳はGoogleカレンダーに記載されており毎日Chromebookからそれを確認している
  • 親もそれを確認しているが、見落とす事があり忘れ物が増えている
  • 翌日分を前日に定期的に出力させたい

達成する為には

  • 閲覧権限のみのGoogleカレンダーPythonで取得する
  • サーマルプリンタに値を渡す
  • crontabなどで曜日指定で定期的に出力させる

試してみた

  • Google Cloud でプロジェクト作成
  • Google Calendar API」を有効化
  • デスクトップアプリ用の OAuth クライアントID を作成し、credentials.json を作業ディレクトリに保存

  • tokenを取得+カレンダーIDを見つける

import datetime
import os

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

# 読み取り専用スコープ
SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"]


def get_credentials():
    """
    Google Calendar API の認証情報を取得
    - 初回はブラウザでOAuth認証
    - 2回目以降は token.json から再利用
    """
    creds = None
    token_path = "token.json"

    if os.path.exists(token_path):
        creds = Credentials.from_authorized_user_file(token_path, SCOPES)

    # 認証情報が無い or 無効ならログイン
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                "credentials.json", SCOPES
            )
            creds = flow.run_local_server(port=0)

        # 次回以降のために保存
        with open(token_path, "w") as token:
            token.write(creds.to_json())

    return creds


def list_calendars(service):
    """
    自分が閲覧可能なカレンダー一覧を表示
    共有カレンダーもここに出てくるので、summary と id をメモしておく
    """
    calendar_list = service.calendarList().list().execute()
    print("=== カレンダー一覧 ===")
    for cal in calendar_list.get("items", []):
        print(f"summary: {cal.get('summary')}, id: {cal.get('id')}")
    print("======================")


def get_events(service, calendar_id, max_results=10):
    """
    指定カレンダーの今以降のイベントを取得して表示
    """
    try:
        now = datetime.datetime.now(tz=datetime.timezone.utc).isoformat()
        events_result = (
            service.events()
            .list(
                calendarId=calendar_id,
                timeMin=now,
                maxResults=max_results,
                singleEvents=True,
                orderBy="startTime",
            )
            .execute()
        )
        events = events_result.get("items", [])

        if not events:
            print("イベントが見つかりませんでした。")
            return

        print(f"=== カレンダーID: {calendar_id} のイベント ===")
        for event in events:
            summary = event.get("summary", "(タイトル未設定)")
            description = event.get("description", "(説明未設定)")
            start = event["start"].get("dateTime", event["start"].get("date"))
            end = event["end"].get("dateTime", event["end"].get("date"))
            location = event.get("location", "(場所未設定)")

            print(f"タイトル : {summary}")
            print(f"説明     : {description}")
            print(f"開始     : {start}")
            print(f"終了     : {end}")
            print(f"場所     : {location}")
            print("-----")

    except HttpError as error:
        print(f"API エラー: {error}")


def main():
    # 認証
    creds = get_credentials()
    service = build("calendar", "v3", credentials=creds)

    # まずは自分が見えるカレンダー一覧を出す(共有カレンダー含む)
    list_calendars(service)

    # ここに、取得したい共有カレンダーIDを貼る
    # 例: "xxxxxxx@group.calendar.google.com"
    calendar_id = input("取得したいカレンダーIDを入力してください: ").strip()

    # 予定取得
    get_events(service, calendar_id, max_results=10)


if __name__ == "__main__":
    main()
  • 取得したtokenとカレンダーIDを使って下記で翌日分を取得します
import datetime
from zoneinfo import ZoneInfo  # Python 3.9+
import json
import requests
from google.oauth2.credentials import Credentials

TOKEN_PATH = "token.json"
SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"]
CALENDAR_ID = "XXXXXXXXXXXXXXX@group.calendar.google.com"
TZ = "Asia/Tokyo"


def load_token_credentials():
    """
    token.json から Credentials を復元
    """
    creds = Credentials.from_authorized_user_file(TOKEN_PATH, SCOPES)
    return creds


def refresh_if_needed(creds: Credentials):
    """
    アクセストークンが切れていたら更新して token.json に保存
    """
    if not creds.valid and creds.refresh_token:
        from google.auth.transport.requests import Request as GoogleRequest

        creds.refresh(GoogleRequest())
        with open(TOKEN_PATH, "w") as f:
            f.write(creds.to_json())
    return creds


def get_day_range(target_date: datetime.date | None = None):
    """
    target_date 当日の 00:00~翌日 00:00(Asia/Tokyo)の
    timeMin, timeMax を RFC3339 文字列で返す
    デフォルトは「明日」
    """
    today = datetime.datetime.now(ZoneInfo(TZ)).date()
    if target_date is None:
        target_date = today + datetime.timedelta(days=1)

    start = datetime.datetime.combine(
        target_date,
        datetime.time(0, 0, 0),
        tzinfo=ZoneInfo(TZ),
    )
    # timeMax は「開始時刻の上限(排他的)」なので、
    # 対象日の翌日 0:00 を指定する
    end = start + datetime.timedelta(days=1)

    return start.isoformat(), end.isoformat()


def get_tomorrow_events_raw(access_token: str):
    """
    requests を使って Google Calendar API を直接叩く
    """
    time_min, time_max = get_day_range()  # デフォルトで「明日」
    # print(f"timeMin: {time_min}")
    # print(f"timeMax: {time_max}")

    url = f"https://www.googleapis.com/calendar/v3/calendars/{CALENDAR_ID}/events"
    params = {
        "timeMin": time_min,
        "timeMax": time_max,
        "singleEvents": "true",
        "orderBy": "startTime",
        "maxResults": 50,
        # 念のためレスポンスのタイムゾーンも固定したい場合
        "timeZone": TZ,
    }
    headers = {
        "Authorization": f"Bearer {access_token}",
    }

    resp = requests.get(url, headers=headers, params=params, timeout=5)
    # print(f"HTTP {resp.status_code}")
    if not resp.ok:
        print(resp.text)
        return None

    return resp.json()


def show_events(events_json: dict):
    items = events_json.get("items", [])
    if not items:
        print("指定日のイベントはありません。")
        return

    for event in items:
        summary = event.get("summary", "(タイトル未設定)")
        description = event.get("description", "(説明未設定)")
        start = event["start"].get("dateTime", event["start"].get("date"))
        end = event["end"].get("dateTime", event["end"].get("date"))
        location = event.get("location", "(場所未設定)")

        print(f"タイトル : {summary}")
        print(f"説明     : {description}")
        print(f"開始     : {start}")
        print(f"終了     : {end}")
        print(f"場所     : {location}")
        print("-----")


def main():
    creds = load_token_credentials()
    creds = refresh_if_needed(creds)

    events_json = get_tomorrow_events_raw(creds.token)
    if events_json:
        show_events(events_json)


if __name__ == "__main__":
    main()
  • 上記を前々回作成したfree.py(引数に出力したいテキストを受け取ってサーマル側で印刷)に渡してあげる
  • 下記を参照

tameshitemita.hatenablog.jp

  • python3 gcal.py | python3 free.py で問題無くサーマルプリンタから印刷が出来た!!
  • あとはiPhoneのショートカットで設定。これもssh+コマンドの組み合わせだけにした

  • crontabの設定
0 19 * * 0-4 /home/pi/dev/thermal/venv/bin/python /home/pi/dev/thermal/gcal.py | /home/pi/dev/thermal/venv/bin/python /home/pi/dev/thermal/free.py

まとめ

  • サーマルプリンタでやれる事が増えて活用度が上がった
  • Google Calendarに情報を集約していけば更に活用が進みそう