一 月
27
土曜日

ドアの動き検知システムの構築3

前回の『ドアの動き検知システムの構築2』ではAWS IoTのルールを作成しドアの動き情報を時系列データとしてS3に保存しAmazon Athenaから検索できるようになった。
今回はこれに加えドアの動きをSlackへ通知する機能を追加する。

今回の目的

今回の目的は2つ。

  • Slackへの通知機能追加
  • サーバサイドClojureScriptでFigwheelを試す

Slackへの通知機能追加

AWS IoTのルールアクションを追加、ドアの動きと連動してSlack通知用AWS Lambdaが呼び出されるようにする。
一回のドア開閉ではドアの細かい動きに連動し数回の加速度情報が発生する。
これを全てSlackへ通知してもあまり意味が無いのでドアの動きを検知した際に前回の検知日時から3分以上経っていた場合のみSlackへの通知を行うようにする。

サーバサイドClojureScriptでFigwheelを試す

『ClojureScriptでLambd@Edge』『打刻情報をKinesis FirehoseでS3へ保存』でもClojureScriptでLambdaを書いたが、REPL駆動開発時におけるコードのリロード方法について課題が残っていた。

今回課題だったのがREPL環境。 REPL自体はcljs.repl/repl*を直接使いNode.js環境のREPLを問題無く利用する事ができたのだが、 肝心のREPL駆動開発時におけるコードのリロード方法のセオリーが分からなかった。 (tools.namespaceやfigwheelのような)

ということで今回は致し方なく素朴にrequireをreloadフラグ付きで利用した。

Figwheelといえばフロントエンド開発で利用するイメージしか無かったが最近ではサーバサイド開発でも利用できる
今回はFigwheelを使用してどのような塩梅なのかを確認する。

Slackの設定

LambdaからSlackのAPIを叩く際に使用するBotユーザを登録する。

https://my.slack.com/services/new/bot

Username
Botユーザ名

Add Bot Integeration で登録。

API TokenはSlackのAPI呼び出し時に使用するのでメモしておく。

Save Integration で保存。


Slackのフリープランには最大10個のアプリとインテグレーションまでという制約がある。
また特に機能に応じて個別のBotを払い出す必要も無いためインテグレーション数の節約のため『TwitterのリストタイムラインをSlackへ連携する』で作成したボットchabonzeのAPI Tokenを利用することにした。

Lambda実行環境の準備

ドアの動き情報をSlackへ通知するLambda関数 naruko-slack-notifier の実行環境を整えるため以下を行う。

  • ロールの作成
  • S3バケットの作成

ロールの作成

Lambda関数 naruko-slack-notifier を実行する為のロールを定義する。

AWS マネージメントコンソールから IAM を開き ロール から 新しいロールの作成 AWSサービス から Lambda を選択。

次のステップ:アクセス権限

ポリシー AWSLambdaBasicExecutionRole を検索し選択。

次のステップ:確認

ロール名
naruko-slack-notifier

ロールの作成 で作成完了。

S3バケットの作成

Lambdaのデプロイパッケージを配置するためのバケットを作成する。

AWS マネージメントコンソールから S3 を開き バケットを作成する

バケット名
naruko.nijohando.jp
リージョン
Tokyo

次へ 、プロパティ設定、アクセス許可設定はスキップし バケットを作成 を実行。

Lambdaの実装

ClojureScriptでLambda naruko-slack-notifier を作成。

Figwheelを利用するための準備

figwheelを利用するため、project.cljlein-figwheelプラグインを追加。

:plugins [[lein-figwheel "0.5.14"]]

dev dependenciesに figwheel-sidecar を追加。

:profiles {:dev {:dependencies [[figwheel-sidecar "0.5.14"]

今回はlein-cljsbuildを使わずにcljs.build.api を直接使いビルドしているためFigwheelについてもScriptingで実行している。

src/dev/clj/tools.cljrepl関数からfigwheelを起動するようにした。

(defn repl
  []
  (ra/start-figwheel!
    {:figwheel-options {} ;; <-- figwheel server config goes here 
     :build-ids ["dev"]   ;; <-- a vector of build ids to start autobuilding
     :all-builds          ;; <-- supply your build configs here
     [{:id "dev"
       :figwheel true
       :source-paths [cljs-src-dir]
       :compiler cljs-dev-compiler-opts}]})
  (ra/cljs-repl))

またサーバサイドClojureScriptでFigwheelを利用する場合、npmパッケージ ws が必要となる。
上記Figwheelの :compiler オプションで指定した cljs-dev-compiler-opts には以下のように npm-deps で ws を指定するようにしている。

(def cljs-npm-deps {})
(def cljs-dev-npm-deps {:ws "4.0.0"})
(def cljs-compiler-opts {:output-dir cljs-out-dir
                         :output-to cljs-output-to
                         :optimizations :simple
                         :source-map cljs-source-map
                         :npm-deps cljs-npm-deps
                         :install-deps true
                         :target :nodejs
                         :verbose true})
(def cljs-dev-compiler-opts (merge cljs-compiler-opts {
                                   :main cljs-main-ns
                                   :npm-deps cljs-dev-npm-deps
                                   :optimizations :none
                                   :source-map true}))

project.cljにタスク エイリアス fwrepl を追加し、上記 repl関数を呼び出すようにする。

:aliases {"fwrepl" ["run" "-m" "clojure.main" "src/dev/clj/tools/repl.clj"]

以上でFigwheelを利用する準備が整った。

Figwheelを利用する

Leiningenからfigwheelを起動する。

$ lein fwrepl

Figwheel: Starting server at http://0.0.0.0:3449
Figwheel: Watching build - dev
Compiling "target/out/index.js" from ["src/main/cljs"]...
Installing Node.js dependencies
・・・
Successfully compiled "target/out/index.js" in 9.989 seconds.
Launching ClojureScript REPL for build: dev
Figwheel Controls:
          (stop-autobuild)                ;; stops Figwheel autobuilder
          (start-autobuild [id ...])      ;; starts autobuilder focused on optional ids
          (switch-to-build id ...)        ;; switches autobuilder to different build
          (reset-autobuild)               ;; stops, cleans, and starts autobuilder
          (reload-config)                 ;; reloads build config and resets autobuild
          (build-once [id ...])           ;; builds source one time
          (clean-builds [id ..])          ;; deletes compiled cljs target files
          (print-config [id ...])         ;; prints out build configurations
          (fig-status)                    ;; displays current state of system
          (figwheel.client/set-autoload false)    ;; will turn autoloading off
          (figwheel.client/set-repl-pprint false) ;; will turn pretty printing off
  Switch REPL build focus:
          :cljs/quit                      ;; allows you to switch REPL to another build
    Docs: (doc function-name-here)
    Exit: Control+C or :cljs/quit
 Results: Stored in vars *1, *2, *3, *e holds last exception object
Prompt will show when Figwheel connects to your application

ClojureScriptからJavaScriptがコンパイルされFigwheelサーバが立ち上がる。

別のコンソールからコンパイルで生成されたindex.jsをNode.jsで実行。

$ node target/out/index.js

Figwheel: trying to open cljs reload socket
Figwheel: socket connection established

Figwheelサーバへの接続が確立した旨のメッセージが表示され、先ほどFigwheelサーバを起動したコンソール側でREPLが利用できるようになる。

Prompt will show when Figwheel connects to your application
To quit, type: :cljs/quit
dev:cljs.user=> 

Figwheelサーバをあげアプリケーション側が接続しREPLが利用可能になるというフロントエンド開発時のブラウザREPLと同じ要領。
ClojureScriptを修正して保存する度にJavaScriptへリコンパイルされNode.js側へ反映、REPLから変更後のコードを操作できる。

Lambdaのデプロイ

パッケージの作成

デプロイパッケージの作成処理についてもsrc/dev/clj/tools.cljの中で自前で行っている。

(defn package
  []
  (clean-node-modules)
  (build)
  (do*
    (let [zip (ZipOutputStream. (jio/output-stream (str target-dir "/" archive-file-name)))
           _ (defer (.close zip))
           main-js (jio/file cljs-output-to)
           source-map (jio/file cljs-source-map)
           append (fn [^String entry-name ^File f]
                    (.putNextEntry zip (ZipEntry. entry-name))
                    (jio/copy f zip)
                    (.closeEntry zip))]
      (append (.getName main-js) main-js)
      (append (.getName source-map) source-map)
      (doseq [f (file-seq (jio/file "node_modules")) :when (.isFile f)]
        (append (.getPath f) f)))))

開発時はFigwheelが必要とする ws が node_modules 配下へインストールされている。
パッケージングの際はこのような dev dependencies なnpmパッケージを含めないようにするため clean-node-modules で一度 node_modules を削除し本番用の :npm-deps で node_modules を再構築しパッケージングしている。
もう少し良き方法がないものかと思っているが一旦はこの方法で。。。

fwreplの時と同様にproject.cljにタスク エイリアス package を追加し、上記 package関数を呼び出すようにする。

:aliases { ;;他のタスク エイリアス・・・
          "package" ["run" "-m" "clojure.main" "src/dev/clj/tools/package.clj"]}
 

パッケージの配備

Lambda関数 naruko-slack-notifier のデプロイパッケージをAWS上へ配備する。
今回はCIによる自動デプロイは行わず手動によるデプロイとする。

またデプロイについてはnervous-systems/cljs-lambdaを利用する方法もあるが、今回は食わず嫌いで利用を見送りとした。

デプロイパッケージの作成

lein package

S3に配置

aws s3 cp target/naruko-slack-notifier.zip s3://naruko.nijohando.jp

lambdaの登録

AWS マネージメントコンソールから Lambda を開き 関数の作成

一から作成 からを選択。

名前
naruko-slack-notifier
ランタイム
Node.js 6.10
ロール
既存のロールを選択
既存のロールを選択
naruko-slack-notifier

関数の作成

次にトリガーを設定する。

トリガーの追加 から Aws IoT を選択。

トリガーの設定を行う。

IoTタイプ
カスタムIoTルール
ルール
NarukoDoor1FirehoseS3
トリガーの有効化

追加 でトリガを追加し 一旦 保存 で関数を保存する。

再度 関数 naruko-slack-notifier を選択し残りの設定を行う。

関数コード から

コードエントリタイプ
Amazon S3からのファイルのアップロード
ランタイム
Node.js 6.10
ハンドラ
index.handler
S3リンクのURL
https://s3-ap-northeast-1.amazonaws.com/naruko.nijohando.jp/naruko-slack-notifier.zip

環境変数 から naruko-slack-notifier の実行に必要な環境変数を定義する。

SLACK_API_TOKEN
SlackのAPIトークン
SLACK_CHANNEL
通知したいチャネル名 (例: #home )
SLACK_USERNAME
Slack上の表示されるメッセージ送信者名 (例: door1 )
DETECTION_THRESHOLD
Slackへ通知する条件の閾値(秒)
ドアが動いた時間から前回ドアが動いた時間の差(秒)がこの値以上であればSlackへ通知を行う。
(例: 180 )

テスト して 保存


以上でドア開閉のタイミングでSlackへ通知が来るようになり、元々やりたかったことが全て出来る様になった。

現在は玄関ドアを対象にして運用しているが郵便受けや宅配ボックスも対象にしたら色々捗りそうだ。
構成的にもRaspberry Pi3MONOSTICKは今のままで TWELITE 2525Aを買い足すだけで対応できるので折を見て他へ展開してみようと思う。