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を動かしています。
microSDカードを押し込むとカチッと言って固定され、もう一回押し込むと今度は出てくるよくあるやつなんですが、ぼくは何を思ったか取り出すときにそのまま引っ張りだしてしまい、それ以来microSDカードが固定されなくなってしまったのでセロテープでごまかしています。
中身の全体像はこんなかんじです。
- crontabで午前3時にrakeタスクをキック
- rakeタスクで当日のGoogleカレンダーを調べ、最初の予定から逆算して起きるべき時刻を求める
- ActiveJobで
mpg321
コマンドをたたくだけのジョブをキューイングする
mpg321
コマンドはmp3の再生ができるコマンドで、再生が終わるまでプロセスが生き続けるのでActiveJobを使って遅延実行するのが相性よさそうでした。
ぼくは最近何を作るにもまず rails new
するクセがあります。「今回の要件でそんな重量級フレームワークを使わなくてもいいじゃん」という声もありそうですが、非同期に実行する仕組みをいれて、DBとのやりとりできるようにして、わかりやすいようにディレクトリ構成はRailsをインスパイアしたような感じで……とかやってるといつも「Railsでええやん」ってなるのでもう最初からRailsでやります。
対象読者・前提知識
- Rails をさわったことがある
- ActiveJob が動く環境がある(Redis+Sidekiq的な? この構築方法は本稿では触れません)
準備
今回は自分専用なので1つだけのアカウントの情報を取ればよいですのでOAuthなどはやりません。下記手順でファイルをダウンロードした場合は、APIクライアントなどの作成は不要です。
- プロジェクトを作成する
- Google Calendar APIを有効化する
- サービスアカウントキーを作成しP12ファイルをダウンロード
- 作成されたメールアドレス的なもの( @project-name.iam.gserviceaccount.com )にカレンダーの閲覧権限を付与(Google Appsアカウントの場合、カレンダーの共有がデフォルトでできなくなっているので先にその設定変更が必要でした)
実装
まずはGemfileに下記書いて bundle install
gem 'google-api-client', require: 'google/api_client'
カレンダーから情報を取る部分です。Rails前提なので require
書いてないし ActiveSupport::TimeWithZone
とかつかってます。
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
コマンドが通るようにしておけばこれだけ。手抜き実装です。
class Mp3PlayerJob < ActiveJob::Base
queue_as :default
def perform(file)
system("mpg321 '#{file}'")
end
end
Googleカレンダーの予定を調べてMp3PlayerJobをキューイングする部分
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経由で呼び出すだけです。
namespace :alarm do
desc '予定を取得して目覚ましをセットする'
task set: :environment do
AlarmService.set
end
end
しばらく使ってみて気になるところ改善しつつ運用していきたいと思います。