在宅ワークが中心の生活になったことで、家の環境を快適にしたい気持ちが半分、電子工作への興味半分で、室内の空気をモニタリングする仕組みを構築しました。
自宅で使うお遊び程度の道具なので、基本的に「動けばいい」でやっています。拙い点は目を瞑ってください。
作ったもの
15分に一回、センサーを使って気温・湿度・気圧を測定し、結果をサーバに保存。さらに Slack へ結果を通知します。
コンポーネントの解説
システムを構成する主要なコンポーネントは以下の通りです。
- BME280
- obniz
- Google App Engine
- slack
BME280
温度・湿度・圧力センサです。 基盤に実装されたモジュールの状態で数百円から購入することができます。
似た製品として、圧力センサのないBMP280もあります。
obniz
obniz はクラウドに特化した IoT 開発の仕組みです。
一見した印象は arduino に近いマイコンボードですが、obniz が指すものは単にボードだけではなく、
ボードにインストールされる obniz OS や、そこに接続される各種デバイスを制御する SDK や、プログラムを配置・実行するクラウド環境を含めた統合的な仕組みのことです。
使用したボードは obniz board 1Y です。
旧世代のモデルと比べるとスリープ機能が加わっていて、消費電力を抑えたまま長時間待機することが可能です。
スリープからの復帰はプログラム制御でき、時間経過や本体のスイッチ操作など任意のタイミングで行えます。
Google App Engine
GCP のプロダクトの一つです。
フルマネージド型のサーバーレスなプラットフォーム上で、高度なスケーラビリティを備えたアプリケーションを構築します。
だそうです。どういう意味でしょうかね。
その他の GCP プロダクトと統合するための SDK が提供されていて、規定のプログラム言語を使ってアプリケーションを構築できます。
インフラの管理面をあまり気にかけることなくアプリケーション開発に注力できることが魅力だと思います。
今回は Go を使って API を開発し、Datastore にデータの永続化を行いました。
slack
もはや説明不要ですかね。いわゆるチャットコミュニケーションツールです。業務やコミュニティユースで幅広く使われています。
僕は仕事はもちろん家庭のコミュニケーションも slack グループを作成して使っています。
slack 上で動作する様々なアプリがサードパーティ含めて多数存在し、また自作することもできます。
今回は、Web hook として利用可能なボット機能を使いました。
アーキテクチャ
作成したシステムの全体像は以下の図の通りです。
次の手順で作成しました。
BME280 と obniz の回路作成
BME280 は HiLetgo というブランドを選びました。
この製品はピンヘッダが付属していますが、自分ではんだ付けする必要があります。
また、obniz のドキュメントで紹介されている製品とは別のものなので、ピンの順番と数が違いますので注意して工作します。
BME280 のピンと、 obniz のソケットのアサインは以下の通りに行います。
BME280 | obniz |
---|---|
VCC | 0 |
GND | 2 |
SCL | 5 |
SDA | 4 |
CSB | 3 |
SDO | 6 |
obniz への接続は、ドキュメントによると直結させずプルアップ抵抗を実装するように指示がありましたので、その通りに行います。
BME280 | JS Parts Library | obniz
1KΩの抵抗を2つ用意して、SCL と SDA それぞれから VCC へ配線するようにはんだ付けをします。
ピンヘッダやユニバーサル基盤がなかったので、ジャンパワイヤの被覆を途中で剥いて直接はんだ付けしています。真似しないほうがいいと思います。
この後抵抗をマスキングテープで絶縁するなどの誤魔化し加工をして完了です。
配線できたら、BME280 側の各ピンからジャンパワイヤを使って obniz に接続します。
ハードウェアの工作は以上です。
ケースの工作
埃をかぶったりしないようケースに入れたいと思い、いらなくなったトランプのケースを加工しました。
電源コードを通す穴と、BME280のセンサー部分を外気に露出させるための穴を開けました。
GAE 上に API を作成
obniz からデータを送信するための API を Go で実装します。
基本的に以前書いた gin で構築する方法を用いますので、本記事では特筆すべき点だけを記載します。
前回の記事と併せて読んでください。
ルーター
package router import ( "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/linlymatsumura/HouseMonitor/pkg/handler" "time" ) func Build() *gin.Engine { router := gin.New() router.Use(cors.New(cors.Config{ AllowMethods: []string{ "POST", "OPTIONS", }, AllowHeaders: []string{ "Access-Control-Allow-Headers", "Authorization", }, AllowOrigins: []string{ "http://obniz.io", "https://obniz.io", }, MaxAge: 24 * time.Hour, })) basicAuthUser := gin.Accounts{ "BASIC_AUTH_USER": "BASIC_AUTH_PASSWORD", } authorized := router.Group("/", gin.BasicAuth(basicAuthUser)) authorized.POST("/api/v1.0.0/air_condition_report", handler.PostAirConditionReport) return router }
ルーターでは CORS の設定を追加する必要があります。
これは obniz がデータをポストしてくる際 obniz.io のドメインから送ってくるためです。
なんとなくBasic認証もかけておきました。
ハンドラ
package handler import ( "fmt" "github.com/gin-gonic/gin" "github.com/linlymatsumura/HouseMonitor/pkg/model/airConditionReport" "github.com/linlymatsumura/HouseMonitor/pkg/util/env" "math" "net/http" "strconv" ) func PostAirConditionReport(c *gin.Context) { err := c.Request.ParseForm() if err != nil { c.String(http.StatusBadRequest, "error") return } temperature, err := strconv.ParseFloat(c.Request.Form["temperature"][0], 64) if err != nil { c.String(http.StatusBadRequest, "error") return } humidity, err := strconv.ParseFloat(c.Request.Form["humidity"][0], 64) if err != nil { c.String(http.StatusBadRequest, "error") return } pressure, err := strconv.ParseFloat(c.Request.Form["pressure"][0], 64) if err != nil { c.String(http.StatusBadRequest, "error") return } device := c.Request.Form["deviceName"][0] secret := c.Request.Form["key"][0] if secret != env.Get().AirConditionReportSecret { c.String(http.StatusBadRequest, "error") return } err = airConditionReport.Save(c, temperature, humidity, pressure, device) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": err, }) return } c.String(http.StatusOK, "ok") }
リクエストパラメータは、
- 気温
- 湿度
- 気圧
の各数値の他、
- デバイス名称(任意の文字列)
を取ります。
これは別の部屋にもセンサーを設置した場合に識別できるようにすることを想定しているためです。
その他、簡単ですがあらかじめ決めておいたシークレットキーを取るようにしました。
保存する前にキーの照合をしてから書き込むようにします。
GAE から Slack へのメッセージ送信機能作成
Slack へのメッセージ送信は Incoming Webhook を使います。
公式ドキュメントに詳しく記載がありますので、設定手順は割愛します。
設定を終えると、Slack へメッセージ送信するための URL を得られますので、それを使います。
package slackUtil import ( "bytes" "fmt" "net/http" ) func SendMessage(content string) error { jsonStr := fmt.Sprintf(`{"text":"%v"}`, content) req, err := http.NewRequest( "POST", "SLACK_WEBHOOK_URL", bytes.NewBuffer([]byte(jsonStr)), ) if err != nil { return err } req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() return err }
上記のようなユーティリティを作ってハンドラの最後で呼び出します。
package handler import ( "fmt" "github.com/gin-gonic/gin" "github.com/linlymatsumura/HouseMonitor/pkg/model/airConditionReport" "github.com/linlymatsumura/HouseMonitor/pkg/util/env" "github.com/linlymatsumura/HouseMonitor/pkg/util/slackUtil" "math" "net/http" "strconv" ) func PostAirConditionReport(c *gin.Context) { // 中略 err = airConditionReport.Save(c, temperature, humidity, pressure, device) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": err, }) return } // slack へ送信 formatTemperature := roundUp(temperature, 1) formatHumidity := roundUp(humidity, 0) formatPressure := roundUp(pressure, 0) message := fmt.Sprintf("🌡 環境モニタリングレポート(%v)\n - 気温: %v ℃\n - 湿度: %v %\n - 気圧: %v hPa", device, formatTemperature, formatHumidity, formatPressure, ) slackUtil.SendMessage(c, message) c.String(http.StatusOK, "ok") } func roundUp(num, places float64) float64 { shift := math.Pow(10, places) return roundUpInt(num*shift) / shift } func roundUpInt(num float64) float64 { t := math.Trunc(num) return t + math.Copysign(1, num) }
obniz から API へデータを送信するプログラムの作成
obniz のプログラムは HTML/Javascript を使って作成します。
obniz がオンラインのとき、ブラウザ経由でプログラムにアクセスし、デバイスを操作したりできます。
また別の方法として、obniz クラウドが提供するサーバレスイベントによる実行方法があります。
あらかじめ決められたスケジュールや、 obniz がオンラインになったことなどをトリガーとして、特定のプログラムを実行できます。
今回は以下のプログラムを作成しました。
- obniz がオンラインになった時
- センサーを起動して値を取得
- API へデータを送る
- 完了後、15分間スリープモードにする
オンラインになったイベントをトリガーにしてこのプログラムを起動することで、15分おきに起動・測定・保存を繰り返す仕組みにしました。
<html> <head> <script src="https://obniz.io/js/jquery-3.2.1.min.js"></script> <script src="https://unpkg.com/obniz@3.4.0-beta.0/obniz.js" crossorigin="anonymous"></script> </head> <body> <div id="obniz-debug"></div> <h1>環境モニタリング</h1> <div id="print"></div> <script> var obniz = new Obniz("OBNIZ_ID", { access_token:"OBNIZ_ACCESS_TOKEN" }); obniz.onconnect = async function () { var bme280 = obniz.wired("BME280", {vio:0, vcore:1, gnd:2, csb:3, sdi: 4, sck: 5, sdo:6 }); await bme280.applyCalibration(); const val = await bme280.getAllWait(); obniz.display.clear(); obniz.display.print('temp: ' + val.temperature); obniz.display.print('humi: ' + val.humidity); obniz.display.print('pres: ' + val.pressure); $.ajax({ url: 'API_URL', type: 'post', beforeSend: function (xhr) { xhr.setRequestHeader ("Authorization", "Basic " + btoa("BAIC_AUTH_USER: BAIC_AUTH_PASSWORD")); }, data: { temperature: val.temperature, humidity: val.humidity, pressure:val.pressure, key: 'SECRET_KEY', deviceName: 'LivingRoom' } }) .done(function () { obniz.display.clear(); obniz.display.print('success'); obniz.sleepMinute(15); }) .fail(function () { obniz.display.clear(); obniz.display.print('faild'); obniz.sleepMinute(15); }) } </script> </body> </html>
懐かしい jQuery.ajax() ですね。
obniz の開発者コンソールでサーバレスイベントの設定を行います。
完成
これでデバイスをコンセントに接続すれば、定期的にレポートが取得できるようになりました。
併置したアナログの温度湿度計と近似した値が取れているので、概ね正確なデータが取れていると思います。
永続化したデータをグラフィカルに表示したり、特定の閾値の時だけメンションを出すような機能追加をやりたいと思っています。
データを可視化してみると「ご飯を炊くと湿度が上がる」というような、
考えてみれば当然のことではありながら、通常意識することのない環境変化に気付くなどの発見が面白いですよ。