Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

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

背景

現代人は時間に追われて生活しています。「目覚めた時刻が起きる時刻」みたいな生活を毎日送ることは難しいです。そこで、多くの人は「起きなければいけない時刻に起きられる仕組みとしての目覚まし時計」を使っていることかと思います。しかし、紀元前に発明された目覚まし時計の延長線上にある現代の目覚まし時計には大きな問題があります。それは、

_人人人人人人人人人人人人人人人_
> セットし忘れると鳴らない! <
 ̄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

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

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

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

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 コマンドが通るようにしておけばこれだけ。手抜き実装です。

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

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

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

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経由で呼び出すだけです。

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

  task set: :environment do
    AlarmService.set
  end
end

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

参考文献

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした