IoTを駆使したCO2濃度の見える化

# はじめに
新型コロナ拡大防止のために技術者として何かできないかなー？と考えていましたが、CO2センサーを使った換気状況の見える化なら自分のスキルを活かせると思い、この記事を執筆しました。
比較的安く高性能な換気見える化システムがDIYで簡単に作れる記事です。

# CO2センサー選定基準
- NDIR方式であること
 - 安い中華のTVOC方式では測定誤差が大きすぎるためCO2値が全く信用できないらしい。
- 何らかの方法でインターネットに接続できること。
 - クラウドからCO2値を参照したいので当然。
- 測定データのアクセス方法などが公開されていること。
 - プロプライエタリなプロトコルではDIYできないので仕様がオープンなもの。
- IFTTTなどと連携が可能なもの。
 - Googleスプレッドシートに自動でデータ蓄積したい。
 - ゆくゆくはSonoffなどと連携してCO2濃度に応じて自動換気扇とか

ということで条件に合うCO2センサーを探したところ、最近販売が開始された換気エアミエルというCO2センサーがBluetooth Low Energyを使ってビーコンデータを送信していて、そのビーコンフォーマットも無償公開されているのでこれを使うことにした。CO2測定誤差も±(30ppm+3%)と結構精度が高いし、温湿度も精度が高くていい感じ。
https://www.stepone.co.jp/products/kanki-airmier/
https://www.amazon.co.jp/gp/product/B08YMM14F8

ただ、そのままではインターネット側に測定データを送信できないのでビーコン⇒インターネットへのゲートウェイが必要になるが、そのへんが超簡単に構築できるobnizというIoT機器を使った。
今回作る全体の構成はこんな感じ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1118291/db187218-87cd-5c22-a6db-e29b3451a2ae.png)

obniz使ってIFTTT連携なんて何番煎じだ？というぐらい先人たちによる記事がたくさんあるけど、逆に言うとノウハウが蓄積されているので労せず作れそう。
うーむ、既存のものをちょちょいと組み合わせるだけで簡単に作れるなんて、いい時代になったもんだ。
IFTTT連携は無料枠が3アプレットしか使えなくなったのは残念だけど、なんだかんだ言ってちょっとやってみるぶんには一番使いやすい。
本気でやるならIFTTT経由せずにobnizのJavaScriptから直接GoogleスプレッドシートのAPI叩けばよろし。

# 換気エアミエルのビーコンフォーマット
換気エアミエルのビーコンドキュメントはここからダウロードできます。
<https://www.stepone.co.jp/products/kanki-airmier/so-bt-01-beaconspec.pdf>

ドキュメントによると、ビーコンフォーマットはAD TypeがManufaturer Specific Dataで独自フォーマットになっている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1118291/10ef4542-d74f-1b52-9d7e-55d9545eaf49.png)
CO2値や温湿度値がほとんどそのまま物理値として格納されているので受信したビーコンから必要部分だけ抜き出して簡単に使えそう。
Sequence Numberとは何ぞや？と思ったら図があった。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1118291/83853301-7a7d-2085-1757-a104423045b3.png)
ふむふむ、ビーコンの取りこぼしを少なくするため同じ測定値を9パケットほど連続して送ってきてるのね。んで、同じデータではSequence Numberが同じなので重複チェックに使え、と。
これならビーコンセンサーあるあるな「全然ビーコンが受信できへんやん！」という問題も少なそう。

というか、なんとBLEのLong Rangeに対応してるじゃありませんか！
いま世に出ているほとんどのスマホはLong Rangeに対応してないはずだけど、何に使う気なんだろう？
Long Range対応のビーコン受信機を作ったら測定データを1キロメートルとか飛ばせそう。またいつか実験してみよう。

# IFTTTの設定
IFTTTの概要や登録、設定はググれば山のように情報があるので、ここでは詳細は割愛して必要な部分のみ記載します。

- ThisはWebhooksで、Event Nameは換気エアミエル本体の裏に貼ってあるシールの"AM_XXXXXXXXXXXX"を指定します。Event Nameを本体固有名にするのは複数のセンサーからデータ取得できるようにするため。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1118291/d7a7c5d9-f98e-0882-b9df-694edd756607.png)


- ThatはGoogle SheetのAdd rowにします。各項目は以下のようにしてください。背景が網掛けの入力部分は実際には"{{EventName}}"のように波括弧を2重にして囲むか、Add ingredientから選択して入力してください。
  Formatted rowが複雑ですが、これはIFTTT標準の日付時刻独自形式ではGoogleスプレッドシートで日付時刻として認識されないため変換しています。
こちらを参考にさせていただきました。https://qiita.com/komi360/items/35769ec65b4b06380d00
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1118291/acffd245-e63c-9502-e7e8-844893dc7049.png)

```text:コピペして使ってね
=DATEVALUE(GOOGLETRANSLATE(left("{{OccurredAt}}",find(" at ","{{OccurredAt}}")),"en","ja"))+TIMEVALUE(RIGHT("{{OccurredAt}}",len("{{OccurredAt}}")- find(" at ","{{OccurredAt}}")-3)) ||| {{Value1}} |||{{Value2}} ||| {{Value3}}
```

できあがったら、<a href="https://ifttt.com/maker_webhooks" target="_blank" rel="noopener noreferrer">Webhooksの画面</a>で右上のDocumentationをクリックし、Your key is:の後にあるWebhooksへのアクセスキーを確認し、どこかにコピペしておいてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1118291/fd38e0ae-a186-b7dc-aa0c-69bb0eb184e9.png)

# obnizでビーコン受信
やってきたビーコンをクラウドに送信するためにobnizを使う。
obnizはIoTハードウェアをクラウドまたはWebブラウザのJavaScriptから制御して爆速開発を可能にするという<del> 変態 </del>画期的なシステムで、Qiitaにもobnizについてはたくさんの記事があると思うので詳細はそちらに任せます。

obnizはAmazonでもスイッチサイエンスでも秋月通商でも売っているので調達は容易。
obniz BLEゲートウェイってのもあるみたいでそっちのほうが簡単なのかもしれないが、今回はお安いobniz Boardを使った。
というか今回初めてobnizを使うのでobniz Boardを買った後にobniz BLEゲートウェイがあるのを知った。

obnizでアプリ(HTML/JavaScript)を新しく作成する時は、開発者コンソールの左上のメニューからアプリ開発を選択し、新規作成をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1118291/10823e81-7421-311e-7110-770c031fa86f.png)

次にブラウザアプリ(HTML/JavaScript)タブのobniz Board/1Yを使うテンプレートから空のプロジェクトを選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1118291/e8176733-1172-0950-8a2a-412b8344dcf2.png)

アプリ名は何でも良いけど今回はAirMierLoggerにしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1118291/8b0d0a54-8014-4cb2-1d13-3bf5f8d8dbf5.png)

10分に1回アプリをクラウドで実行して欲しいので、アプリ設定で時間で実行に"every/10minutes"と入力する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1118291/84204d20-fdfc-a2a3-1b9e-57bf35d01a32.png)
ちなみに、通常obnizのJavaScriptを実行しているのは我々ユーザーサイドのWebブラウザなので、実行しているWebブラウザのページを閉じてしまうとobnizの実行も止まってしまいます。この「クラウド実行」というのはユーザーサイドがWebブラウザやPCを起動しなくても代わりにobnizサーバーサイドで自動的によしなに起動してくれる便利な機能です。
ただし1日に150回までという制限があるけど、10分毎に1回起動だと1日で144回起動の計算になるのでセーフ。

設定が終わったら プログラムを編集 をクリック。
で、表示されたhtmlを以下のコードにごっそり置き換え。

```html:AirMierLogger

<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
    />
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script
      src="https://unpkg.com/obniz@3.x/obniz.js"
      crossorigin="anonymous"
    ></script>
  </head>
  <body>
    <script>
     const obniz = new Obniz("OBNIZ_ID_HERE");
     const your_ifttt_key = "YOUR_IFTTT_KEY";           // 置き換えてね
     var airmier_address_list = ["YOUR_AIRMIER_BDADDRESS"];// 置き換えてね（全部小文字で！）、複数も可

     obniz.onconnect = async function () {
       obniz.display.font("Tahoma", 12);
       obniz.display.clear();
       obniz.display.print("Scanning...");

       await obniz.ble.initWait();

       while (airmier_address_list.length > 0) {
         var peripheral = await obniz.ble.scan.startOneWait(
           { deviceAddress: airmier_address_list },
           { filterOnDevice: true }
         );
         if (peripheral != null) {
           var data = getData(peripheral);
           obniz.display.drawing(false);
           obniz.display.clear();
           obniz.display.print(data.name);
           obniz.display.print(data.co2 + " ppm");
           obniz.display.print(data.temperature + " ℃");
           obniz.display.print(data.humidity + " %RH");
           obniz.display.drawing(true);

           // 取得できたセンサーをリストから削除
           airmier_address_list = airmier_address_list.filter(
             (address) => address !== peripheral.address
           );

           // IFTTT WebhookへGET METHOD発行
           var url =
             "https://maker.ifttt.com/trigger/" +
             data.name +
             "/with/key/" +
             your_ifttt_key;
           $.get(url, {
             value1: data.co2,
             value2: data.temperature,
             value3: data.humidity,
           });
         } else {
           obniz.display.print("Timeout.");
           break;
         }
       }
       obniz.display.print("Done.");
       if (typeof done === "function") {
         done();
       }
     };

     function getData(peripheral) {
       var co2 = peripheral.adv_data[6] * 256 + peripheral.adv_data[5];
       var temperature =
         (peripheral.adv_data[8] * 256 + peripheral.adv_data[7]) / 10.0;
       var humidity =
         (peripheral.adv_data[10] * 256 + peripheral.adv_data[9]) / 10.0;
       var name = new TextDecoder().decode(
         new Uint8Array(peripheral.adv_data.slice(16))
       );
       return {
         co2: co2,
         temperature: temperature.toFixed(1),
         humidity: humidity.toFixed(1),
         name: name,
       };
     }
</script>
  </body>
</html>
```

JavaScript部分先頭のYOUR_IFTTT_KEYにIFTTTの設定でコピペしておいたWebhooksへのアクセスキー文字列を、YOUR_AIRMIER_BDADDRESSには換気エアミエルの物理アドレスと置き換える。換気エアミエルの物理アドレスは本体の裏に貼ってあるシールの"AM_XXXXXXXXXXXX"の12桁のX部分が該当する。
どうもobnizのアドレス比較は小文字で行っているみたいなので、物理アドレスは全部小文字にしないとダメみたい。
例えば"AM_ABCDEF012345"なら"abcdef012345"をYOUR_AIRMIER_BDADDRESSと置き換え。
リストで複数の物理アドレスを指定することも可能だけど、その場合はIFTTTのアプレットも同様に複数作っておいてね。

JavaScriptでやってることは、obnizに接続した後、BLEから指定された物理アドレスのビーコン受信を待ち、受信したビーコンをごにょごにょした後に
　Value1: CO2値　　Value2: 温度値　　Value3: 湿度値
が入ったIFTTTのWebhookが呼ばれるコードになっています。

あとは作ったアプリを自分のobnizにインストールすればOK！

# Googleスプレッドシート
正常にビーコンを受信できればGoogleドライブの
　マイドライブ/IFTTT/MakerWebhooks/AirMierLog/
の下に換気エアミエル本体名と同名のGoogleスプレッドシートが作られ、シート1に10分毎のデータが追加されていきます。
1行目に列ヘッダとして説明を挿入してもちゃんと最終行に追加してくれています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1118291/04d9ceca-c1d4-0687-7623-2d30b2474132.png)
無料枠のIFTTTを経由しているので遅延があるかと思ったけどまったく問題ないみたい。Webhooksだとあまり遅延はないのかな？

シート2にグラフ追加するとさらに良い感じ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1118291/11daa197-4693-8be4-c50c-6ce1242bd7d1.png)

グラフ作成のコツは、参照するデータ範囲を現在データが入っている行＋１まで指定しておくとIFTTTからAdd Rowされてもグラフの参照範囲が自動的に拡張されるので再設定の必要がありません。

# 最後に
長期的に運用しているとたまにビーコンを取り逃してアプリステータスがTimeoutになることがあるけど、3日間ほど連続動作させても問題なく動作しています。
Googleスプレッドシートにはグラフ公開機能があり、グラフだけの参照を別のHTMLに埋め込んだりできたりするので使い方によっては複数拠点のCO2や温湿度を管理拠点で一括モニタできたり、店舗や施設のホームページで換気状況をリアルタイムに公開して安全性をアピールする、なんて応用も考えられます。

温湿度が取得できるのでGoogleスプレッドシート上で暑さ指数を計算し、換気とともに熱中症対策にも使えそうです。

初期投資に3万円弱の機器購入が必要とは言え、ランニングコストゼロでここまでのことができるのはすごい時代になりましたね。