Googleカレンダーを使ったRuby製自動目覚ましを作ってみた

Advent Calendar 書くの遅れてすみません|ω･ o)

## 背景

現代人は時間に追われて生活しています。「目覚めた時刻が起きる時刻」みたいな生活を毎日送ることは難しいです。そこで、多くの人は「起きなければいけない時刻に起きられる仕組みとしての目覚まし時計」を使っていることかと思います。しかし、[紀元前に発明された](https://ja.wikipedia.org/wiki/%E6%99%82%E8%A8%88%E3%81%AE%E6%AD%B4%E5%8F%B2#.E7.B4.80.E5.85.83.E5.89.8D500.E5.B9.B4.E3.81.8B.E3.82.89.E7.B4.80.E5.85.831.E4.B8.96.E7.B4.80)目覚まし時計の延長線上にある現代の目覚まし時計には大きな問題があります。それは、

＿人人人人人人人人人人人人人人人＿
＞　セットし忘れると鳴らない！　＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y￣

もっと言うと、

＿人人人人人人人人人人人人人人人＿
＞　曜日指定すると祝日も鳴る！　＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y￣

など柔軟性に欠けます。起きなくてよい日に自分の意志とは関係なく起こされることほど気分を害することはありません。

## 解決策

ぼくは何度も裏切られてきたので自分の記憶力をまったく信用しておらず、カレンダーを見ながらでないと予定を入れません。つまり、ぼくが何時に起きないといけないかはカレンダーは知っています。なのにもかかわらず目覚まし時計をセットするという運用はみなさん大好きなDRYの原則に反します。

と、いうことでGoogleカレンダーの予定を読み取り、最初の予定の1時間半前に目覚ましが鳴る自動目覚ましを作ってみました。

## 仕組み

Raspberry PiでRailsを動かしています。

![CW5IGi5UEAAkv-7.jpg_large.jpg](https://qiita-image-store.s3.amazonaws.com/0/19478/e99215e5-a6b9-aa5a-a090-d05e8352e808.jpeg)

microSDカードを押し込むとカチッと言って固定され、もう一回押し込むと今度は出てくるよくあるやつなんですが、ぼくは何を思ったか取り出すときにそのまま引っ張りだしてしまい、それ以来microSDカードが固定されなくなってしまったのでセロテープでごまかしています。

中身の全体像はこんなかんじです。

1. crontabで午前3時にrakeタスクをキック
2. rakeタスクで当日のGoogleカレンダーを調べ、最初の予定から逆算して起きるべき時刻を求める
3. ActiveJobで `mpg321` コマンドをたたくだけのジョブをキューイングする

`mpg321` コマンドはmp3の再生ができるコマンドで、再生が終わるまでプロセスが生き続けるのでActiveJobを使って遅延実行するのが相性よさそうでした。

ぼくは最近何を作るにもまず `rails new` するクセがあります。「今回の要件でそんな重量級フレームワークを使わなくてもいいじゃん」という声もありそうですが、非同期に実行する仕組みをいれて、DBとのやりとりできるようにして、わかりやすいようにディレクトリ構成はRailsをインスパイアしたような感じで……とかやってるといつも「Railsでええやん」ってなるのでもう最初からRailsでやります。

## 対象読者・前提知識

- Rails をさわったことがある
- ActiveJob が動く環境がある（Redis+Sidekiq的な？　この構築方法は本稿では触れません）

## 準備

今回は自分専用なので1つだけのアカウントの情報を取ればよいですのでOAuthなどはやりません。下記手順でファイルをダウンロードした場合は、APIクライアントなどの作成は不要です。

1. プロジェクトを作成する
2. Google Calendar APIを有効化する
3. サービスアカウントキーを作成しP12ファイルをダウンロード
4. 作成されたメールアドレス的なもの（ @project-name.iam.gserviceaccount.com ）にカレンダーの閲覧権限を付与（Google Appsアカウントの場合、カレンダーの共有がデフォルトでできなくなっているので先にその設定変更が必要でした）

## 実装

まずはGemfileに下記書いて `bundle install`

```ruby:Gemfile
gem 'google-api-client', require: 'google/api_client'
```

カレンダーから情報を取る部分です。Rails前提なので `require` 書いてないし `ActiveSupport::TimeWithZone` とかつかってます。

```ruby:app/services/google_calendar_service.rb
class GoogleCalendarService
  # 今日の最初のイベントの開始時刻を返す
  # @return [ActiveSupport::TimeWithZone]
  def self.today_first_event_start_time
    today_first_event.start.date_time.in_time_zone
  end

  # 今日の最初のイベントを返す
  # @return [Google::APIClient::Schema::Calendar::V3::Event]
  def self.today_first_event
    today_events.first
  end

  # 今日のイベントをすべて返す
  # @return [Array<Google::APIClient::Schema::Calendar::V3::Event>]
  def self.today_events
    cal = client.discovered_api('calendar', 'v3')
    client.execute(
      api_method: cal.events.list,
      parameters: {
        calendarId: '(カレンダーID/自分の場合はGoogleアカウントのメールアドレスでした)',
        orderBy: 'startTime',
        timeMin: Time.current.beginning_of_day.iso8601,
        timeMax: Time.current.end_of_day.iso8601,
        singleEvents: 'True'
      }
    ).data.items
  end

  # カレンダーの読み取りができる Google::APIClient を返す
  # @return [Google::APIClient]
  def self.client
    Google::APIClient.new(application_name: '').tap do |client|
      key = Google::APIClient::PKCS12.load_key('(ダウンロードしたp12ファイルまでのパス)', 'notasecret')
      client.authorization = Signet::OAuth2::Client.new(
        token_credential_uri: 'https://accounts.google.com/o/oauth2/token',
        audience: 'https://accounts.google.com/o/oauth2/token',
        scope: 'https://www.googleapis.com/auth/calendar.readonly',
        issuer: '(@project-name.iam.gserviceaccount.comのメールアドレス)',
        signing_key: key
      )
      client.authorization.fetch_access_token!
    end
  end
end
```

アラームを鳴らす部分の処理は、 `mpg321` コマンドが通るようにしておけばこれだけ。手抜き実装です。

```ruby:app/jobs/mp3_player_job.rb
class Mp3PlayerJob < ActiveJob::Base
  queue_as :default

  def perform(file)
    system("mpg321 '#{file}'")
  end
end
```

Googleカレンダーの予定を調べてMp3PlayerJobをキューイングする部分

```ruby:app/services/alarm_service.rb
class AlarmService
  # 最初の予定の何分前に鳴らすか？
  PRIOR_TIME = 90.minutes

  def self.set(prior_time = PRIOR_TIME)
    alarm_time = GoogleCalendarService.today_first_event_start_time - prior_time
    Mp3PlayerJob.set(wait_until: alarm_time).perform_later('(鳴らしたいMP3ファイルまでのパス)')
  end
end
```

あとはこれをrake task経由で呼び出すだけです。

```ruby:lib/tasks/alerm.rake
namespace :alarm do
  desc '予定を取得して目覚ましをセットする'

  task set: :environment do
    AlarmService.set
  end
end
```

しばらく使ってみて気になるところ改善しつつ運用していきたいと思います。

## 参考文献

- [rubyでgoogleカレンダーのイベントを取得するメモ - Qiita](http://qiita.com/mechamogera/items/bf2ed20e332dc31d2352)
- [Heroku上からService Account認証でGoogle APIにアクセスする - すぎゃーんメモ](http://d.hatena.ne.jp/sugyan/20130112/1357996092)