俺のアウトプット

調べたこと、試したことを書きます

AWS Cloud9が東京リージョンでサポートされました

今朝方、AWS Cloud9 でEC2タイプのUbuntu環境を検証していたのですが。。

おもむろにリージョンを選択すると、東京リージョンが選べるではないですか。

 

f:id:kitsugi:20190405085158j:image

 

f:id:kitsugi:20190405092859j:image

 

今までは東京リージョンのサービスを使う場合、シンガポール等で環境を構築し、そこを経由して東京リージョンへアクセスしていました。

 

東京リージョンがサポートされたことにより、よりレイテンシが小さくなります。

 

これでサポートされるリージョンは6つ。


* 米国東部 (バージニア北部)
* 米国東部 (オハイオ)
* 米国西部 (オレゴン)
* アジアパシフィック (シンガポール)
* アジアパシフィック (東京)
* EU (アイルランド)

 

少し前に、EC2環境でUbuntuを選択出来るようにもなりました。

 

今年は、AWS Cloud9 が、そしてブラウザ開発が熱くなりそうですね。

詳細は後ほどまとめようと思います。

 

4/6 追記

公式に発表されました。

https://aws.amazon.com/jp/about-aws/whats-new/2019/04/aws-cloud9-is-now-available-in-asia-pacific--tokyo--region/

 

Raspberry Pi タクトスイッチでポチッとな 〜LEDをON/OFF〜

前回は、最初の一歩として Lチカ を作成しました。

今回は、入力に タクトスイッチ を使って、LEDのON/OFFにチャレンジします。

  • スイッチを押したらLEDが点灯 (ON)
  • スイッチを離したらLEDが消灯 (OFF)

用意したもの

  • ブレッドボード
  • タクトスイッチ
  • ジャンパワイヤ (オス-メス) 4本
  • ジャンパワイヤ (U字型単線タイプ) 2本
  • LED 1本
  • 抵抗 (200Ω)

タクトスイッチは、押すと「カチッ」というクリック感があり、それが心地よく、ついつい何度も押してしまいます。
カバンに入れて持ち歩き、暇な時に連打するといいかもしれません。

プルダウン抵抗とプルアップ抵抗

タクトスイッチを使う場合、入力値が不定にならないように、プルダウン抵抗かプルアップ抵抗を用いる必要があります。

  • プルダウン抵抗
    • スイッチOFF: LOW (0V)
    • スイッチON: HIGH (3.3V)
  • プルアップ抵抗
    • スイッチOFF: HIGH (3.3V)
    • スイッチON: LOW (0V)

ブレッドボード上に大きめの抵抗を配置することで実現できますが、Raspberry Pi の場合、本体側にプルダウン抵抗・プルアップ抵抗が用意されています。

今回は、Raspberry Pi 本体のプルダウン抵抗を利用します。 その分、回路図がシンプルになります。

回路図

3.3V と GPIO24 の間にタクトスイッチを接続します。

f:id:kitsugi:20190105212124p:plain

ブレッドボード

前回より少し複雑になっています。
今回は横ラインに電源用とGND用に利用しました。

f:id:kitsugi:20190105221206p:plain

プログラム

import RPi.GPIO as GPIO
import time

PIN_IN = 24
PIN_OUT = 25
GPIO.setmode(GPIO.BCM)
GPIO.setup(PIN_IN, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(PIN_OUT, GPIO.OUT)

try:
    while True:
        flag = GPIO.input(PIN_IN) == GPIO.HIGH
        GPIO.output(PIN_OUT, flag)
        time.sleep(0.01)
except KeyboardInterrupt:
    pass
finally:
    GPIO.cleanup()

入力設定

入力時にプルダウン抵抗を利用するため、setup関数にキーワード引数 pull_up_down を指定します。

ループ内

GPIO24 の入力値に電流が流れてるかどうかをフラグにして、GPIO25 の出力を制御します。
CTRL+C で処理を中断できます。

ラズパイで確認

スイッチがOFFの状態です。LEDはOFFの状態です。

f:id:kitsugi:20190105153510j:plain

ポチッとな!!

f:id:kitsugi:20190105153527j:plain

LEDが光りました。 まるで天津飯の太陽拳のようです。

参考

Raspberry Pi にチャレンジ 〜LEDでチカチカ〜

今更ですが、Raspberry Pi (ラズパイ) を購入しました。

前々から興味はあったのですが、ラズパイを使って何かをする目的がないので、購入にためらっていました。

昔、ちょっとだけ触って、そのままフェードアウトしていった、玄人志向のKURO-BOXを思い出します。

しかし、友達にラズパイの勉強会に誘われたのを機に購入。

考え方を変えて

まず触ってみる。触ってから目的を考える。

ことにしました。

目的が出来るまで、ちょっとずつ触って、チャレンジしてみようかと思います。

購入したもの

環境構築

  • Raspbian OSをインストール
  • Python3環境の構築
  • SSH接続

SDカードは相性があるようですが、シリコンパワーのSDカードでも普通にいけました。

Lチカって

まずは、最初のステップとして Lチカ にチャレンジ。

Lチカとは、LEDをプログラムで制御して点滅させる、シンプルな構造。
LEDをチカチカさせるのでLチカ。ローソンのはLチキ

最初にトライするには、うってつけです。

用意したもの

  • ブレッドボード
  • ジャンパワイヤ (オス-メス) 2本
  • LED 1本
  • 抵抗 (200Ω)

抵抗は300前後であれば問題ありません。

GPIOポート

ラズパイの GPIO から電源を取ります。
GPIOとは汎用入出力インターフェイスのこと。General-Purpose Input/Output の略。

ラズパイの場合、40本のピンがあり、図の左下から順に

https://www.raspberrypi.org/documentation/usage/gpio/images/gpio-pins-pi2.jpg

2 4 6 838 40
1 3 5 737 39

と、ピン番号が割り当てられています。

各GPIOには、用途が決まっています。

https://www.raspberrypi.org/documentation/usage/gpio/images/gpio-numbers-pi2.png

例えば、ピン番号 1 は 3V の電源が取れます。
ずっと光らせるだけであれば、このピン番号 1 にジャンパワイヤを接続して電源を取得するだけです。

ですが、LEDをチカチカさせたい場合は、プログラム上からON/OFFを制御します。 このような場合は、黄色い丸の GPIOから取得します。

用途 ピン番号 ピン番号 用途
3V (1) (2) 5V
GPIO2 (3) (4) 5V
GPIO3 (5) (6) GND
GPIO4 (7) (8) GPIO14
GND (9) (10) GPIO15
GPIO17 (11) (12) GPIO18
GPIO27 (13) (14) GND
GPIO22 (15) (16) GPIO23
3V (17) (18) GPIO24
GPIO10 (19) (20) GND
GPIO9 (21) (22) GPIO25
GPIO11 (23) (24) GPIO8
GND (25) (26) GPIO7
GPIO0 (27) (28) GPIO1
GPIO5 (29) (30) GND
GPIO6 (31) (32) GPIO12
GPIO13 (33) (34) GND
GPIO19 (35) (36) GPIO16
GPIO26 (37) (38) GPIO20
GND (39) (40) GPIO21

Lチカ

fritzing を利用して回路図を作成します。

今回は GPIO24 から電源をとり、LEDの間に抵抗を置きます。

f:id:kitsugi:20190102103218p:plain

回路図からブレッドボード図を微調整して、イメージを抑えます。

f:id:kitsugi:20190102103233p:plain

プログラム

PythonのGPIOモジュールを利用して、1秒ごとに点灯、消灯を10回繰り返すプログラムを作成します。

import RPi.GPIO as GPIO
import time

PIN = 24
GPIO.setmode(GPIO.BCM)
GPIO.setup(PIN, GPIO.OUT)

try:
    for i in range(10):
        GPIO.output(PIN, GPIO.HIGH)
        time.sleep(1.0)
        GPIO.output(PIN, GPIO.LOW)
        time.sleep(1.0)
except KeyboardInterrupt:
    pass
finally:
    GPIO.cleanup()

GPIOモード

ピン番号で指定する BOARD と、GPIOの数字で指定する BCM があります。

今回は BCM を利用します。

GPIO24 を出力として利用するため、セットアップでは 24 を指定します。
もし、BOARD であれば、18 を指定します。

点灯・消灯

GPIO.output メソッドの第二引数で電流のON/OFFを設定します。

  • GPIO.HIGH (True)
  • GPIO.LOW (False)

ラズパイで確認

ブレッドボード図をもとに、実際に接続し、SSH経由でプログラムを実行します。

f:id:kitsugi:20190102103251j:plain

無事、LEDをチカチカすることができました!

まとめ

大学時代に、半田ごてを使って回路を作ったはずなのですが、遠い昔のことで、すっかり忘れておりました。
楽しかった記憶もありません。

しかし、実際にLEDが点灯したときは、純粋に感動し、楽しいと感じました。 今回はLチカですが、次のステップを試してみたいと思います。

ラズパイを始めるにあたり、カラー図解-Raspberry-Piで学ぶ電子工作-作って動かしてしくみがわかる が、大変わかりやすく、とても参考になりました。
おすすめの本です。

参考

2019年はチャレンジの年 〜筑波連山縦走〜

新年明けましておめでとうございます。

俺事(私事)ですが、昨年は、やりたい事のために、意を決して転職活動をしました。
縁あって、2月から新しい職場、環境になります。

大きく環境が変わるため、早く仕事に慣れて、色々とチャレンジをしたい。
今から楽しみです。

今月はすべて有給休暇を取れたので、勉強や体力作り、リフレッシュ等に充てたいと思います。

『一年の計は元旦にあり』
ということで、さっそく元旦から体力作りにチャレンジをしました!

『筑波連山縦走』
水戸線の岩瀬駅から御嶽山に登り、尾根伝いに筑波山へ目指します。前からチャレンジしたかったことの1つです。

筑波連山縦走

4時に家を出発。
元旦なのに、家にいなくて家族に申し訳ない。。。お土産買って帰るから許してね。

岩瀬駅

暗い、誰もいない、そして寒い。。。 トイレの水が凍ってました。

ここから筑波山へ目指します。

御嶽山登山口

登山口に来ました。まだ暗いです。

看板右上に筑波山が。
ヘッドライトをしながら登って行きます。

御嶽山 (230m)

おんたけさん と読みます。 木曽の御嶽と関係があるのでしょうか。

しかし、こちらの御嶽はあっけなく山頂に到着です。
空が少しずつ明るくなってきました。 ここから尾根伝いにひたすら歩きます。

雨引山 (409m)

本日のベストショット。
右側の山が筑波山。遠いなぁ。

燕山 (701m)

苦労して登った割には展望なし。
途中、茂みの方から大きなガサガサ音がして、心臓バクバク。
何のフレンズだったんだろう。。

加波山 (709m)

加波山神社で初めて人に遭遇。 少しホッとしました。

ここで初詣。

神々しい道を通って頂上付近。たばこ神社という、珍しい神社がありました。

加波山に到着。
ここまでは順調でした。ここまでは。

桜観音

丸山を経て一本杉峠へ行く予定でしたが、道を間違えて、加波山登山口の桜観音へ。
完全に別方向で、しかも入口まで降りてしまい、へこみました。
_| ̄|○

リタイアするか続行するか?

悩んだ末に、続行を決意。
ただし、同じ道は戻りたくないので、林道経由で一本杉峠へ目指します。

この辺りは採石場のようです。

一本杉峠 (430m)

何とか一本杉峠に到着。 しかし、かなりの体力と時間をロスしてしまいました。

今は冬なので、16:30には暗くなってしまいます。

  • 筑波山からはケーブルカーで降りる
  • ガスを使った昼食を諦めて、携帯食とお菓子でさっと済ます

この作戦でロスした時間を補うことにしました。

足尾山 (627m)

足尾山 山頂から筑波山方面を望む。
今日中にあそこにたどり着けるのか。。

きのこ山 (528m)

展望なし。今日の山の中で1番期待はずれ。
とっとと先を進みます。

上曽峠〜裏筑波登山口

ひたすら林道をず〜っと歩く。長い。
コンクリなので、かえって足が痛い。

裏筑波登山口〜筑波高原キャンプ場

倒木だらけで進みにくい道。
あまり使われてないルートなのかな。

倒木「ヒャッハー!ここは通さねえぜ」

筑波高原キャンプ場に到着。
疲労でかなり足にきてるけど、後は気合で登るだけ。

筑波山 (877m)

やっと到着!!
疲れたけど、充実感があって、やってよかったです。

でも来年の元旦は家族と過ごします(笑)

時間がないので女体山だけ登って、ケーブルカーで降りました。ギリギリ、つくば駅への最終バスに間に合いました。 お腹減った〜。

さいごに

 今回の工程とタイムです。

工程 標高 タイム
岩瀬駅 6:00
御嶽山登山口 6:15
御嶽山 230m 6:25
雨引山 409m 7:10
燕山 701m 8:45
加波山 709m 9:15
丸山 528m ※道を間違える
一本杉峠 430m 11:05
足尾山 627m 11:35
きのこ山 528m 12:20
上曽峠 320m 12:50
裏筑波登山口 13:38
筑波高原キャンプ場 14:35
筑波山 877m 15:40

1日でこんなに歩いたのは久しぶりです。

ZappaでKappaを表示してみた

このエントリは、AWS #2 Advent Calendar 2018 の23日目です。

Zappaとは

Amazon API Gateway と AWS Lambda でWebアプリケーションを簡単に構築、デプロイできるPython用のサーバーレスフレームワークです。
FlaskやDjango、Bottleなど WSGI に対応したフレームワーク上で作成したアプリケーションを、Lambdaで利用できます。

https://camo.githubusercontent.com/be05103c626a5afe18dc4b1208a4b465dbd9e731/687474703a2f2f692e696d6775722e636f6d2f6631504a7843512e676966

Zappa.ioZappatista! が 実際に Zappa を使ってWebサイトを構築しています。

Kappaとは

頭にお皿を乗せた緑色の妖怪。キュウリが大好き。
一部の河童は地下で強制的に寿司を…略

環境構築

前準備として、ローカル環境に以下の環境を構築します。

  • AWS Credential 設定
  • pyenv + pyenv-virtualenv 環境
  • Python 3.6

Flask

ミニマムなFlaskアプリを準備します。

$ pip install flask

画面に Hello World を表示するだけのサンプルです。

hello.py

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello World'

# We only need this for local development.
if __name__ == '__main__':
    app.run()

ローカル上で確認をします。

$ python hello.py

ブラウザ上で http://127.0.0.1:5000/ にアクセスして Hello World が表示されることを確認します。

Zappa

Zappa をインストール、構築します。

$ pip install zappa
$ zappa init
███████╗ █████╗ ██████╗ ██████╗  █████╗
╚══███╔╝██╔══██╗██╔══██╗██╔══██╗██╔══██╗
  ███╔╝ ███████║██████╔╝██████╔╝███████║
 ███╔╝  ██╔══██║██╔═══╝ ██╔═══╝ ██╔══██║
███████╗██║  ██║██║     ██║     ██║  ██║
╚══════╝╚═╝  ╚═╝╚═╝     ╚═╝     ╚═╝  ╚═╝

Welcome to Zappa!

Zappa is a system for running server-less Python web applications on AWS Lambda and AWS API Gateway.
This `init` command will help you create and configure your new Zappa deployment.
Let's get started!

Your Zappa configuration can support multiple production stages, like 'dev', 'staging', and 'production'.

今回は全てデフォルト、Enterキーを押します。

  • 環境
    • What do you want to call this environment (default 'dev'): {Enterキー}
  • デプロイ時に利用するS3バケット名
    • What do you want to call your bucket? (default 'zappa-*********'): {Enterキー}
  • 関数名
    • Where is your app's function? (default 'hello.hello.app'): {Enterキー}
  • グローバル展開するか
    • Would you like to deploy this application globally? (default 'n') [y/n/(p)rimary]: {Enterキー}
  • 確認
    • Does this look okay? (default 'y') [y/n]: {Enterキー}

zappa_settings.json が作成されます。

{
    "dev": {
        "app_function": "hello.hello.app",
        "aws_region": "ap-northeast-1",
        "profile_name": "default",
        "project_name": "zappa-sample",
        "runtime": "python3.6",
        "s3_bucket": "zappa-*********"
    }
}

ここまでで、ディレクトリ構造は下記のようになります。

zappa-sample/
├── .python-version
├── hello
│   └── hello.py
└── zappa_settings.json

デプロイ

それではdev環境にデプロイしてみましょう。

$ zappa deploy dev

成功(Deployment complete)すると、URLが表示されるので、ブラウザで確認をします。
デプロイ簡単ですね。

更新

テンプレートの利用

Flaskアプリをテンプレートから読み込むように変更します。
templates ディレクトリに index.html を用意します。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>sample</title>
</head>
<body>
  <p>{{ message }}</p>
</body>
</html>

hello.py

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html', message="Merry Christmas!!")

# We only need this for local development.
if __name__ == '__main__':
    app.run()

ディレクトリ構造

zappa-sample/
├── .python-version
├── hello
│   ├── hello.py
│   └── templates
│       └── index.html
└── zappa_settings.json

updateコマンドで、dev環境に更新を反映します。

$ zappa update dev

Merry Christmas!!

文字が変わったでしょうか?

静的ファイルの利用

今度は画像ファイルを表示してみます。 staticディレクトリに画像ファイルを配置します。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>sample</title>
</head>
<body>
  <p>{{ message }}</p>
  <img src="{{ url_for('static', filename='youkai_kappa.png') }}">
</body>
</html>

ディレクトリ構造

zappa-sample/
├── .python-version
├── hello
│   ├── hello.py
│   ├── static
│   │   └── youkai_kappa.png
│   └── templates
│       └── index.html
└── zappa_settings.json

f:id:kitsugi:20181222223705p:plain

ZappaでKappaを表示できました!!

静的ファイルは、S3上に配置する方が低コストです。
Flaskの場合だと、Flask-S3 を利用すれば、簡単に実現できます。

アンデプロイ

undeploy で削除します。

$ $ zappa undeploy dev

まとめ

語呂がよかったので勢いで書きましたが、虚しくなってきたので、このへんで勘弁してください。

参考

Fitbit API から睡眠データを取得してみた

Fitbit Charge2 を装着したまま洗顔をしたら、水がかかりお亡くなりになりました。。。

www.fitbit.com

ちょうど、Fitbit Charge3 が出たので購入。 今度は防水なので大丈夫でしょう!!

せっかくなので、睡眠データを取得して何かをしたいと思います。

睡眠レベル

一言に睡眠と言っても、状態によっていくつかレベルがあります。

  • 覚醒状態 (wake)
    • 毎晩10-30回ほど発生
    • 期間が非常に短い
  • レム睡眠 (rem)
    • 浅い眠り、体の眠り
    • 夢を見ることがある
    • 心拍数が上がったり、呼吸が早くなったり
  • ノンレム睡眠
    • 深い眠り、脳の眠り
    • 第1段階〜第4段階に分かれる

眠りの深さは

覚醒状態 -> レム睡眠 -> ノンレム睡眠 第1段階 -> ノンレム睡眠 第4段階

の順に深くなります。

Fitbitでは、ノンレム睡眠を浅い睡眠と深い睡眠の2つに分け、4つの睡眠レベルに分けています。

  1. 目覚めた状態 (wake)
  2. レム睡眠 (rem)
  3. 浅い睡眠 (light)
    • ノンレム睡眠 第1段階
    • ノンレム睡眠 第2段階
  4. 深い睡眠 (deep)
    • ノンレム睡眠 第3段階
    • ノンレム睡眠 第4段階

俺の睡眠データ

基本、早寝早起きです。たまに小学生の子供達より早く寝ます。

手っ取り早く、curlで叩いて睡眠データを取得します。
Fitbit APIをcurlで叩く方法は、下記のエントリを参照してください。

《2018-11-16》

ver1.1以前(classic) と 1.2(stages) ではフォーマットが異なるので注意です。

ver1.1の場合

curl -H "Authorization: Bearer **********.**********" https://api.fitbit.com/1.1/user/-/sleep/date/2018-11-16.json | jq . > 2018-11-16-v1.1.json

2018-11-16-v1.1.json

{
  "sleep": [
    {
      "awakeCount": 0,
      "awakeDuration": 0,
      "awakeningsCount": 17,
      "dateOfSleep": "2018-11-16",
      "duration": 30000000,
      "efficiency": 95,
      "endTime": "2018-11-16T05:39:00.000",
      "isMainSleep": true,
      "logId": 20170274799,
      "minuteData": [
        {
          "dateTime": "21:18:30",
          "value": "1"
        },
        {
          "dateTime": "21:19:30",
          "value": "1"
        },
        略
        {
          "dateTime": "05:36:30",
          "value": "1"
        },
        {
          "dateTime": "05:37:30",
          "value": "1"
        }
      ],
      "minutesAfterWakeup": 0,
      "minutesAsleep": 473,
      "minutesAwake": 27,
      "minutesToFallAsleep": 0,
      "restlessCount": 17,
      "restlessDuration": 27,
      "startTime": "2018-11-15T21:18:30.000",
      "timeInBed": 500
    }
  ],
  "summary": {
    "stages": {
      "deep": 71,
      "light": 215,
      "rem": 85,
      "wake": 41
    },
    "totalMinutesAsleep": 473,
    "totalSleepRecords": 1,
    "totalTimeInBed": 500
  }
}

v1.2の場合

curl -H "Authorization: Bearer **********.**********" https://api.fitbit.com/1.2/user/-/sleep/date/2018-11-16.json | jq . > 2018-11-16-v1.2.json

2018-11-16-v1.2.json

{
  "sleep": [
    {
      "dateOfSleep": "2018-11-16",
      "duration": 30000000,
      "efficiency": 95,
      "endTime": "2018-11-16T05:39:00.000",
      "infoCode": 0,
      "isMainSleep": true,
      "levels": {
        "data": [
          {
            "dateTime": "2018-11-15T21:18:30.000",
            "level": "wake",
            "seconds": 30
          },
          {
            "dateTime": "2018-11-15T21:19:00.000",
            "level": "light",
            "seconds": 300
          },
          略
          {
            "dateTime": "2018-11-16T05:23:00.000",
            "level": "wake",
            "seconds": 510
          },
          {
            "dateTime": "2018-11-16T05:31:30.000",
            "level": "light",
            "seconds": 450
          }
        ],
        "shortData": [
          {
            "dateTime": "2018-11-15T21:18:30.000",
            "level": "wake",
            "seconds": 150
          },
          {
            "dateTime": "2018-11-15T21:59:00.000",
            "level": "wake",
            "seconds": 60
          },
          略
          {
            "dateTime": "2018-11-16T05:10:30.000",
            "level": "wake",
            "seconds": 30
          },
          {
            "dateTime": "2018-11-16T05:33:30.000",
            "level": "wake",
            "seconds": 30
          }
        ],
        "summary": {
          "deep": {
            "count": 6,
            "minutes": 79,
            "thirtyDayAvgMinutes": 72
          },
          "light": {
            "count": 35,
            "minutes": 264,
            "thirtyDayAvgMinutes": 196
          },
          "rem": {
            "count": 10,
            "minutes": 92,
            "thirtyDayAvgMinutes": 64
          },
          "wake": {
            "count": 38,
            "minutes": 65,
            "thirtyDayAvgMinutes": 43
          }
        }
      },
      "logId": 20170274799,
      "minutesAfterWakeup": 0,
      "minutesAsleep": 435,
      "minutesAwake": 65,
      "minutesToFallAsleep": 0,
      "startTime": "2018-11-15T21:18:30.000",
      "timeInBed": 500,
      "type": "stages"
    }
  ],
  "summary": {
    "stages": {
      "deep": 71,
      "light": 215,
      "rem": 85,
      "wake": 41
    },
    "totalMinutesAsleep": 435,
    "totalSleepRecords": 1,
    "totalTimeInBed": 500
  }
}

ver1.2の方が睡眠レベル単位でまとめているため、データ容量が少なくなる傾向になります。
以後、ver1.2形式で取得します。

f:id:kitsugi:20181118161035p:plain

《2018-11-17》

Fitbitを外したまま寝てしまった。。。
しかし、エラーにならず結果が取得できることがわかりました。

curl -H "Authorization: Bearer **********.**********" https://api.fitbit.com/1.2/user/-/sleep/date/2018-11-17.json | jq . > 2018-11-17-v1.2.json

2018-11-17-v1.2.json

{
  "sleep": [],
  "summary": {
    "totalMinutesAsleep": 0,
    "totalSleepRecords": 0,
    "totalTimeInBed": 0
  }
}

《2018-11-18》

curl -H "Authorization: Bearer **********.**********" https://api.fitbit.com/1.2/user/-/sleep/date/2018-11-18.json | jq . > 2018-11-18-v1.2.json

2018-11-18-v1.2.json

{
  "sleep": [
    {
      "dateOfSleep": "2018-11-18",
      "duration": 24840000,
      "efficiency": 89,
      "endTime": "2018-11-18T05:30:00.000",
      "infoCode": 0,
      "isMainSleep": true,
      "levels": {
        "data": [
          {
            "dateTime": "2018-11-17T22:36:00.000",
            "level": "wake",
            "seconds": 30
          },
          {
            "dateTime": "2018-11-17T22:36:30.000",
            "level": "light",
            "seconds": 1020
          },
          略
          {
            "dateTime": "2018-11-18T05:18:00.000",
            "level": "light",
            "seconds": 510
          },
          {
            "dateTime": "2018-11-18T05:26:30.000",
            "level": "wake",
            "seconds": 210
          }
        ],
        "shortData": [
          {
            "dateTime": "2018-11-17T22:36:00.000",
            "level": "wake",
            "seconds": 120
          },
          {
            "dateTime": "2018-11-17T22:45:00.000",
            "level": "wake",
            "seconds": 30
          },
          略
          {
            "dateTime": "2018-11-18T04:32:00.000",
            "level": "wake",
            "seconds": 30
          },
          {
            "dateTime": "2018-11-18T04:35:00.000",
            "level": "wake",
            "seconds": 120
          }
        ],
        "summary": {
          "deep": {
            "count": 4,
            "minutes": 77,
            "thirtyDayAvgMinutes": 73
          },
          "light": {
            "count": 23,
            "minutes": 203,
            "thirtyDayAvgMinutes": 209
          },
          "rem": {
            "count": 8,
            "minutes": 75,
            "thirtyDayAvgMinutes": 70
          },
          "wake": {
            "count": 26,
            "minutes": 59,
            "thirtyDayAvgMinutes": 47
          }
        }
      },
      "logId": 20192347613,
      "minutesAfterWakeup": 0,
      "minutesAsleep": 355,
      "minutesAwake": 59,
      "minutesToFallAsleep": 0,
      "startTime": "2018-11-17T22:36:00.000",
      "timeInBed": 414,
      "type": "stages"
    }
  ],
  "summary": {
    "stages": {
      "deep": 71,
      "light": 215,
      "rem": 85,
      "wake": 41
    },
    "totalMinutesAsleep": 355,
    "totalSleepRecords": 1,
    "totalTimeInBed": 414
  }
}

f:id:kitsugi:20181118161054p:plain

睡眠データの項目

公式ドキュメントを見ても項目についての詳細な説明がなく、項目名と実データから推測。間違っていたらコメントください。

睡眠データ

睡眠データ(sleep)は配列形式です。
昼寝をして夜寝た場合は2件登録されます。複数件ある場合は、Fitbit側が判定して1件だけisMainSleepフラグをtrueにします。

項目 サンプル 説明
dateOfSleep '2018-09-18' 睡眠日
duration 21960000 ベッド(布団)にいた合計時間(ミリ秒)
efficiency 97 睡眠効率(%)。のび太は100。計算方法はここを参考
endTime '2018-09-18T05:06:00.000' 起床時間
infoCode 0
isMainSleep true メインの睡眠かどうか
levels 明細データのレベル
data {"dateTime": "2018-09-18T02:27:00.000", "level": "rem", "seconds": 840} 睡眠レベル単位のデータ配列。wake, rem, light, deepの4種類。睡眠レベル、開始時間、持続時間(秒)
shortData {"dateTime": "2018-09-17T23:04:00.000", "level": "wake", "seconds": 150} 覚醒データ配列。目覚めた状態が3分以内のデータ
summary "deep": {"count": 4, "minutes": 78, "thirtyDayAvgMinutes": 57} 睡眠レベル単位の要約データ。合計回数、合計時間(分)、30日間の平均時間(分)
logId 19548152255 ログID
minutesAfterWakeup 5 起床した後の時間(分)
minutesAsleep 334 睡眠時間(分)
minutesAwake 32 起きていた時間(分)
minutesToFallAsleep 0 最初の眠るまでの時間(分)
startTime '2018-09-17T23:00:00.000' 就寝時間
timeInBed 366 ベッド(布団)にいた時間(分)。minutesAsleep + minutesAwake
type 'stages' 睡眠データの種類

timeInBed = minutesToFallAsleep + minutesAsleep + minutesAwake

要約データ

項目 サンプル 説明
stages 睡眠レベル
deep 38 深い睡眠(分)
light 163 浅い睡眠(分)
rem 82 レム睡眠(分)
wake 31 目覚めた状態(分)
totalMinutesAsleep 334 睡眠時間の合(分)
totalSleepRecords 1 合計睡眠回数(sleep配列の件数)
totalTimeInBed 366 ベッド(布団)にいた合計時間(分)。totalTimeInBed - totalMinutesAsleep が起きている時間

Python3の場合

Fitbit用のモジュールを利用すると簡単に取得できます。

クライアントID (OAuth 2.0 Client ID)、クライアントシークレット、アクセストークン、リフレッシュトークンは https://dev.fitbit.com/apps で登録したアプリから取得した値を設定します。

API_VERSIONに1.2を指定しています。(デフォルト1.0)

import datetime
import fitbit

def main():
    client = fitbit.Fitbit(CLIENT_ID,
                           CLIENT_SECRET,
                           access_token=ACCESS_TOKEN,
                           refresh_token=REFRESH_TOKEN)
    client.API_VERSION = 1.2
    
    dt = datetime.date(2018, 11, 18)
    sleep = client.get_sleep(dt)
    print(sleep)


if __name__ == "__main__":
    main()

参考

ブログに3日間書き込みしなかったら催促メールを送る

AWS Lambdaを使って、定期的にブログのトップページをチェック。 3日間書き込みがなかったら、催促メールを送り続ける。

あまり嬉しくないプログラムを作成します。

概要

大まかな流れは以下になります。

f:id:kitsugi:20181002221128p:plain

  1. CloudWatch Events が定期的にLambda関数を実行
  2. ブログのトップページから最新(最終)エントリ日を取得
  3. 3日間経過していたらメール送信

Lambda関数を作成する前に、下準備をします。

メール送信の準備

メールが送信できるように、Amazon SNSでトピックを作成してメール用のサブスクリプションを準備します。

トピックの作成

マネジメントコンソールから、サービス [アプリケーション統合] - [Simple Notification Service] を選択します。

トピックを選択して、[新しいトピックの作成] ボタンをクリック

f:id:kitsugi:20180805170748p:plain

トピック名を入力して[トピックの作成] ボタンをクリック

f:id:kitsugi:20181003052244p:plain

トピックが作成されます。

サブスクリプションの作成

トピック一覧から作成したトピックのARNリンクをクリックして、トピックの詳細を開きます。

f:id:kitsugi:20181003052611p:plain

[サブスクリプションの作成] ボタンをクリックします。

f:id:kitsugi:20181003052929p:plain

プロトコルとエンドポイントを入力します。

  • プロトコル: Email
  • エンドポイント: 送信したいメールアドレス

f:id:kitsugi:20181003053331p:plain

[サブスクリプションの作成]ボタンをクリックすると、確認用のメールが送信されます。
メール内のConfirm subscriptionリンクをクリックすると確認済となり、これで利用可能となります。

f:id:kitsugi:20181003053702p:plain

メールの確認

メールが受信できるか確認します。
トピックの詳細画面、左上の [トピックに発行] ボタンをクリック

f:id:kitsugi:20181003053932p:plain

件名とメッセージを入力して [メッセージの発行] ボタンをクリック

f:id:kitsugi:20181003054335p:plain

メールが届いたら成功です。
トピックARNは後で利用するので、メモ(コピー)しておいてください。

ロールの準備

Lambdaからメール送信ができるように、IAMロールを作成します。

マネジメントコンソールから、サービス [セキュリティ、 アイデンティティ、 コンプライアンス] - [IAM] を選択します。

サイドパネルの [ロール] から、[ロールの作成] ボタンをクリックします。

ロールを使用するサービスを選択

AWSサービスのLambdaを選択して、[次のステップ:アクセス権限] ボタンをクリックします。

f:id:kitsugi:20181003062548p:plain

ポリシーの割り当て

Lambdaからメール送信ができるように、以下のポリシーにチェックをつけます。

  • AWSLambdaBasicExecutionRole
  • AmazonSNSFullAccess

ポリシーのフィルタで絞り込むと探しやすいです。

f:id:kitsugi:20181003062557p:plain

ロールの確認と作成

ロール名 lambda-check-entry-date を入力し、ポリシーを確認。

問題がなければ、[ロールの作成] ボタンをクリックします。

f:id:kitsugi:20181003062605p:plain

以上で下準備は終わりです。

今回もAWS Cloud9上から作成していきます。

ローカル上にLambda関数を作成

ローカル上にLambda関数を作成します。

右側サイドバーにある、[AWS Resources] を選択して [AWSリソース] ウィンドウを開き、[λ+] ボタンをクリックします。

  • 関数名
    • checkEntryDate
  • アプリケーション名
    • CheckEntryDate
  • ランタイム
    • Python 3.6
  • 設計図
    • hello-world-python3
  • 関数トリガー
    • none
  • メモリ
    • 128MB
  • ロール
    • Choose an existing role
      • lambda-check-entry-date (下準備で作成したロール)

外部モジュールのインストール

Pythonの外部モジュールをインストールします。

  • Requests
    • 使い勝手のよいHTTPライブラリ
  • Beautiful Soup
    • スクレイピングでよく使われるHTMLパーサー
  • dateutils
    • 日付操作用ライブラリ
  • pytz
    • タイムゾーンライブラリ

ターミナルから、CheckBlogディレクトリ直下にインストールします。

$ cd CheckBlog/
$ pip-3.6 install requests -t .
$ pip-3.6 install beautifulsoup4 -t .
$ pip-3.6 install dateutils -t .
$ pip-3.6 install pytz -t .

-tはどこにインストールするかのターゲットを、.はカレントディレクトリを意味します。

必要なモジュールがインストールされました。

template.yaml

template.yaml に環境変数とスケジュールを登録します。

環境変数の使い方は、下記のエントリを参照してください。

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: An AWS Serverless Specification template describing your function.
Resources:
  checkBlog:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: checkBlog/lambda_function.lambda_handler
      Runtime: python3.6
      Description: ''
      MemorySize: 128
      Timeout: 15
      Role: 'arn:aws:iam::123456789012:role/lambda-check-entry-date'
      CodeUri: .debug/
      Environment:
        Variables:
          site: 'https://oreout.hatenablog.com'
          period: '3'
          topic: 'arn:aws:sns:ap-northeast-1:123456789012:check_blog'
          subject: '俺の通知'
          body: |
            何怠けとるねん!
            そろそろブログを更新して!
      Events:
        CheckBlogScheduledEvent:
          Type: Schedule
          Properties:
            Schedule: rate(6 hours)

環境変数

  • site
    • チェックするブログのトップページ
  • period
    • 最終投稿日からの何日間で判定するか
  • topic
    • メール送信の準備て作成したトピックARN
  • subject
    • メール件名
  • body
    • メール本文 (改行する場合はパイプ|を定義して次行に本文)

スケジュール

起動する間隔をRate式で定義します。

rate(数値 単位)

単位は を指定できます。

注意点として、数値が複数の場合は、単位が複数形になります。

Rate式は Rate または Cron を使用したスケジュール式 を参照してください。

Lambda関数

下記コードに差し替えて、保存 (Command+S) します。

サンプルのため、例外処理は考慮していません。

lambda_function.py

import boto3
import os
import requests
from bs4 import BeautifulSoup
from datetime import datetime
from dateutil import relativedelta
from dateutil import parser
from pytz import timezone

SITE = os.environ['site']
PERIOD = int(os.environ['period'])
TOPIC = os.environ['topic']
SUBJECT = os.environ['subject']
BODY = os.environ['body']

def lambda_handler(event, context):
    r = requests.get(SITE)
    soup = BeautifulSoup(r.text, 'html.parser')
    entry = soup.find('time').get('datetime')
    
    # 最後の投稿日
    latest = parser.parse(entry).date()

    # N日前
    target = datetime.now(timezone('Asia/Tokyo')) - relativedelta.relativedelta(days=PERIOD)
    target = target.date()

    if target > latest:
        sns = boto3.client('sns')
        sns.publish(TopicArn=TOPIC, Subject=SUBJECT, Message=BODY)
        return False
    else:
        return True
  • requestsでブログのトップページをGET
  • レスポンス結果をBeautifulSoupに渡してHTMLパース
  • 最初のtimeタグ(投稿日の降順)を取得して、datetime属性の値(日付)を取得
  • 現在日と比較してN日間経過している場合はメール送信

実行とデプロイ

ローカルで実行確認、問題がなければデプロイします。

ローカルの実行、デプロイの仕方は、下記のエントリを参照してください。

これで、ブログをさぼっていたら、催促メールが飛んでくるようになりました。

参考