(更新: 2026.03.21)

複数Googleアカウントのカレンダーを一元管理する方法【OpenClaw連携】

複数Googleアカウントのカレンダーを一元管理する方法【OpenClaw連携】

はじめに

仕事用とプライベート用で複数のGoogleアカウントを使い分けている人は多いと思います。しかし、カレンダーを複数開いて確認するのは面倒ですよね。

そこで今回、複数のGoogleカレンダーを自動監視して、新しい予定をリアルタイムで通知するツールを開発しました。OpenClawという個人用AIアシスタントと連携して、Telegramに自動通知します。

開発の背景

私は以下の2つのGoogleアカウントを使い分けています:

1. クロスリンクアカウント(仕事用)

– 会社の会議・MTG

– 顧客との打ち合わせ

2. プログラミングスクールアカウント(副業用)

– 体験会・研修

– リーダーMTG

これらを常に2つのカレンダーを開いて確認するのが非効率だったため、一元管理できるシステムを構築しました。

システム構成

アーキテクチャ

Google Calendar API
    ↓
Python監視スクリプト(30分ごとに実行)
    ↓
OpenClaw(AIアシスタント)
    ↓
Telegram通知

技術スタック

  • **Python 3.11**
  • **Google Calendar API v3**
  • **OAuth 2.0認証**
  • **OpenClaw** – 個人用AIアシスタント
  • **Cron** – 定期実行
  • 実装のポイント

    1. Google Calendar API の認証

    Google Calendar APIを使うには、OAuth 2.0認証が必要です。

    credentials.json の取得

    1. [Google Cloud Console](https://console.cloud.google.com/)にアクセス

    2. プロジェクトを作成

    3. 「APIとサービス」→「OAuth同意画面」で設定

    4. 「認証情報」→「OAuth 2.0 クライアントID」を作成

    5. `credentials.json`をダウンロード

    認証フロー

    from google.oauth2.credentials import Credentials
    from google_auth_oauthlib.flow import InstalledAppFlow
    from google.auth.transport.requests import Request
    from googleapiclient.discovery import build
    
    SCOPES = [
        'https://www.googleapis.com/auth/calendar.readonly',  # カレンダー読み取り
        'https://www.googleapis.com/auth/calendar.events'     # イベント作成・編集
    ]
    
    def authenticate(account: str):
        """
        Googleアカウントを認証してトークンを保存
        
        Args:
            account: 'crosslink' or 'programming_school'
        """
        token_path = f'token_{account}.json'
        creds = None
        
        # 既存のトークンを読み込み
        if os.path.exists(token_path):
            creds = Credentials.from_authorized_user_file(token_path, SCOPES)
        
        # トークンが無効または存在しない場合、再認証
        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 build('calendar', 'v3', credentials=creds)

    ポイント

  • 各アカウントごとに別々のトークンファイル(`token_crosslink.json`、`token_programming_school.json`)を保存
  • トークンの有効期限が切れたら自動でリフレッシュ
  • 2. カレンダーイベントの監視

    30分ごとに新しいイベントをチェックして、通知します。

    import json
    from datetime import datetime, timedelta
    
    STATE_FILE = 'calendar_monitor_state.json'
    
    def load_state():
        """前回のチェック状態を読み込む"""
        if os.path.exists(STATE_FILE):
            with open(STATE_FILE, 'r') as f:
                return json.load(f)
        
        return {
            'last_check': None,
            'seen_events': {}  # {account: [event_id, ...]}
        }
    
    def check_new_events(account: str, state: dict):
        """
        新しいイベントをチェック
        
        Returns:
            新しいイベントのリスト
        """
        # カレンダーサービスを取得
        service = authenticate(account)
        
        # 前回チェック時刻以降のイベントを取得
        last_check = state.get('last_check')
        if last_check:
            time_min = datetime.fromisoformat(last_check).isoformat() + 'Z'
        else:
            # 初回は現在時刻から
            time_min = datetime.utcnow().isoformat() + 'Z'
        
        time_max = (datetime.utcnow() + timedelta(days=30)).isoformat() + 'Z'
        
        # イベント取得
        events_result = service.events().list(
            calendarId='primary',
            timeMin=time_min,
            timeMax=time_max,
            singleEvents=True,
            orderBy='startTime'
        ).execute()
        
        events = events_result.get('items', [])
        
        # 既に見たイベントを除外
        seen_ids = set(state.get('seen_events', {}).get(account, []))
        new_events = [
            event for event in events
            if event['id'] not in seen_ids
        ]
        
        return new_events
    
    def monitor_calendars():
        """全カレンダーを監視"""
        state = load_state()
        
        accounts = ['crosslink', 'programming_school']
        all_new_events = []
        
        for account in accounts:
            print(f"📅 Checking {account}...")
            
            try:
                new_events = check_new_events(account, state)
                
                if new_events:
                    print(f"  ✨ {len(new_events)} new event(s)")
                    
                    # 見たイベントIDを記録
                    if account not in state['seen_events']:
                        state['seen_events'][account] = []
                    
                    for event in new_events:
                        state['seen_events'][account].append(event['id'])
                        all_new_events.append({
                            'account': account,
                            'event': event
                        })
                else:
                    print(f"  ℹ️ No new events")
            
            except Exception as e:
                print(f"  ❌ Error: {e}")
        
        # 状態を更新
        state['last_check'] = datetime.utcnow().isoformat()
        save_state(state)
        
        # 通知
        if all_new_events:
            notify_new_events(all_new_events)
        else:
            print("\n✅ No new events to notify")
    
    def save_state(state: dict):
        """状態を保存"""
        with open(STATE_FILE, 'w') as f:
            json.dump(state, f, indent=2)

    ポイント

  • 前回チェック時刻を保存して、重複通知を防ぐ
  • 既に見たイベントIDを記録(`seen_events`)
  • 複数アカウントを順次チェック
  • 3. OpenClawへの通知

    OpenClawのcron機能を使って、Telegramに自動通知します。

    def notify_new_events(events: list):
        """
        新しいイベントをOpenClaw経由でTelegram通知
        
        Args:
            events: [{account: str, event: dict}, ...]
        """
        message = "🆕 **新しいカレンダーイベントが追加されました!**\n\n"
        
        for item in events:
            account = item['account']
            event = item['event']
            
            # 日本語のアカウント名
            account_name = {
                'crosslink': 'クロスリンク',
                'programming_school': 'プログラミングスクール'
            }.get(account, account)
            
            # イベント情報を整形
            summary = event.get('summary', '(タイトルなし)')
            start = event['start'].get('dateTime', event['start'].get('date'))
            
            # 日時をパース
            if 'T' in start:
                # 時刻あり
                dt = datetime.fromisoformat(start.replace('Z', '+00:00'))
                time_str = dt.strftime('%m/%d %H:%M')
            else:
                # 終日イベント
                dt = datetime.fromisoformat(start)
                time_str = dt.strftime('%m/%d')
            
            message += f"  {time_str} - {summary}\n"
            message += f"    ({account_name})\n\n"
        
        # OpenClawのwake APIを呼び出し
        import requests
        
        # OpenClawのgateway URLを環境変数から取得
        gateway_url = os.getenv('OPENCLAW_GATEWAY_URL', 'http://localhost:8765')
        
        response = requests.post(
            f'{gateway_url}/api/wake',
            json={'text': message},
            headers={'Content-Type': 'application/json'}
        )
        
        if response.status_code == 200:
            print("✅ Notification sent to OpenClaw")
        else:
            print(f"❌ Failed to notify: {response.status_code}")

    4. Cronによる定期実行

    30分ごとに監視スクリプトを実行:

    #!/bin/bash
    # run_monitor.sh
    
    cd "$(dirname "$0")"
    python3 calendar_monitor.py
    # crontab -e で設定
    */30 * * * * /path/to/run_monitor.sh >> /path/to/logs/calendar_monitor.log 2>&1

    より高度な実装:OpenClaw cronを使う

    # OpenClawのcron機能を使えば、より柔軟な設定が可能
    openclaw cron add \
      --name "Calendar Monitor" \
      --schedule "*/30 * * * *" \
      --command "python3 /path/to/calendar_monitor.py"

    実際の通知例

    Telegramに以下のような通知が届きます:

    🆕 新しいカレンダーイベントが追加されました!
    
      03/10 09:30 - 新ラーニング朝会(Google Meet)
        (クロスリンク)
    
      03/12 14:00 - リーダーMTG
        (プログラミングスクール)
    
      03/15 13:00 - プロシーズさん定例会
        (クロスリンク)

    追加機能

    1. 会議前リマインダー

    30分前、1時間前に自動リマインド:

    def check_upcoming_events():
        """30分以内に始まる予定をチェック"""
        now = datetime.now()
        threshold = now + timedelta(minutes=30)
        
        for account in ['crosslink', 'programming_school']:
            service = authenticate(account)
            
            events_result = service.events().list(
                calendarId='primary',
                timeMin=now.isoformat() + 'Z',
                timeMax=threshold.isoformat() + 'Z',
                singleEvents=True,
                orderBy='startTime'
            ).execute()
            
            events = events_result.get('items', [])
            
            for event in events:
                start = event['start'].get('dateTime')
                if start:
                    start_dt = datetime.fromisoformat(start.replace('Z', '+00:00'))
                    minutes_until = (start_dt - now).total_seconds() / 60
                    
                    if 25 <= minutes_until <= 30:
                        notify_reminder(event, int(minutes_until))
    
    def notify_reminder(event: dict, minutes: int):
        """リマインダー通知"""
        summary = event.get('summary', '(タイトルなし)')
        message = f"🔔 **まもなく予定があるよ!({minutes}分後)**\n\n"
        message += f"📝 {summary}"
        
        # OpenClawに通知
        # ... (省略)

    2. 予定のコンフリクト検出

    同じ時間に複数の予定が入っている場合に警告:

    def detect_conflicts():
        """予定の重複を検出"""
        all_events = []
        
        # 全アカウントのイベントを取得
        for account in ['crosslink', 'programming_school']:
            service = authenticate(account)
            events = get_events(service)
            all_events.extend([(account, e) for e in events])
        
        # 時間でソート
        all_events.sort(key=lambda x: x[1]['start'].get('dateTime', ''))
        
        conflicts = []
        for i in range(len(all_events) - 1):
            event1 = all_events[i][1]
            event2 = all_events[i + 1][1]
            
            end1 = event1['end'].get('dateTime')
            start2 = event2['start'].get('dateTime')
            
            if end1 and start2:
                end1_dt = datetime.fromisoformat(end1)
                start2_dt = datetime.fromisoformat(start2)
                
                if end1_dt > start2_dt:
                    conflicts.append((event1, event2))
        
        if conflicts:
            notify_conflicts(conflicts)

    セキュリティ対策

    1. トークンの安全な保存

    # トークンファイルのパーミッションを制限
    chmod 600 token_*.json

    2. 環境変数での管理

    # .env ファイルを使用
    OPENCLAW_GATEWAY_URL=http://localhost:8765
    OPENCLAW_GATEWAY_TOKEN=your_secret_token_here
    from dotenv import load_dotenv
    
    load_dotenv()
    
    gateway_url = os.getenv('OPENCLAW_GATEWAY_URL')
    gateway_token = os.getenv('OPENCLAW_GATEWAY_TOKEN')

    3. .gitignoreで秘密情報を除外

    # Google Calendar認証
    credentials.json
    token_*.json
    
    # 環境変数
    .env
    
    # 状態ファイル
    calendar_monitor_state.json

    トラブルシューティング

    エラー: `invalid_grant`

    原因:トークンの有効期限切れ

    解決策

    # トークンを削除して再認証
    rm token_*.json
    python server.py
    # → ブラウザで認証

    エラー: `API has not been used`

    原因:Google Calendar APIが有効化されていない

    解決策

    1. Google Cloud Consoleで「APIとサービス」→「有効なAPIとサービス」

    2. 「+ APIとサービスの有効化」をクリック

    3. 「Google Calendar API」を検索して有効化

    まとめ

    複数Googleアカウントのカレンダー統合で得られた効果:

  • ✅ **カレンダーを切り替える手間がゼロに**
  • → 全ての予定を一箇所で確認

  • ✅ **新しい予定の見逃しが激減**
  • → 自動通知で即座に気づく

  • ✅ **会議前のリマインダーで遅刻防止**
  • → 30分前通知で準備できる

  • ✅ **予定の重複を事前に検出**
  • → ダブルブッキングを防ぐ

    Google Calendar APIは非常に強力で、今回紹介した機能以外にも:

  • イベントの自動作成
  • 定期予定の管理
  • 参加者への招待送信
  • など、様々なことが可能です。業務効率化の第一歩として、ぜひ試してみてください!

    ---

    技術スタック: Python, Google Calendar API, OAuth 2.0, OpenClaw

    開発期間: 約1週間

    コード行数: 約1,000行

    対応サービス: Google Calendar