お前ら今すぐ換気しろ！【GAS×Messaging API ×MH-Z19】

# なにやったのか3行で
Raspberry Pi上に二酸化炭素センサ（MH-Z19)を実装し、二酸化炭素濃度を計測した。
Google Spread Sheetへ二酸化炭素濃度を転記し、一定の二酸化炭素濃度を超えた場合、LINE BOTを使って換気を促すシステムをGASを使って構築した。
換気をする時間がわからないため、換気後に一定の二酸化炭素濃度を下回った場合も通知するようにした。


# はじめに
## Motivation
IT業界は他の業種に比べて働く場所の制約が少なく、昨今のご時世によりテレワーク等の制度を使って自宅に引きこもっている人も多いでしょう。（自己紹介）

二酸化炭素濃度が上昇すると、下記のように体に様々な症状が出てくるようになります。（めまい、倦怠感、眠気など）
![CO2_yoko_bar.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111324/cc3cbdaf-b58d-fcba-eb61-f19c874e26cd.png)
（http://group.chcsys.net/jp/chcgroup/ourbrand/co2_controller/ より画像拝借）

そのため、公共の場や職場環境ではCO2濃度に一定の基準が設けられており、おおよそ1000〜1500ppm(1%~1.5%)以下にしなければならないと決まっているようです。

頭が動かないのはエンジニアにとって致命的。効率を高めるために朝活を始めたり、高級椅子を購入するなど、色々やっていますが、二酸化炭素濃度を管理もエンジニアがしなければならないことなのです！


## 二酸化炭素計高い！！
「じゃあ二酸化炭素計買って定量評価してみるか〜」と思い、Amazonを覗いてみると1万円（！）超え。
温度計の感覚（１０００円くらい）で買おうとしていた私はこの段階で躊躇したものの、色々調べてみると二酸化炭素濃度センサが1500円程度で買えることが判明しました。
センサがあるならラズパイとつないで何でもできるじゃん！ということで、センサ情報を取得するだけでなく、換気が必要なタイミングをLINEで通知することで何も考えることなく換気ができるようなシステムを構築しました。




# システム全体像
システム構成図は以下の通りです


<システム構成図>


①ラズパイ上にCO2センサ（MH-Z19）を実装
②ラズパイからGoogle Spread SheetへCO2データを送信
③GASを用いてLINE BOT(Messaging API)を送信




# 二酸化炭素センサの実装
MH-Z19はUARTシリアル通信というもので測定値を取得します。
Raspberry Piの配線は、以下の配線図を参考に行います。
（https://pypi.org/project/mh-z19/ より引用）
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f34363534342f30383238333031342d363864322d633364652d313634342d3763386439623762363266642e6a706567.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111324/0d88b688-7adc-9134-6e27-5c8ae87fdcf0.jpeg)



実装が完了したら以下のコマンドを実行。

```bash
$ sudo echo "enable_uart=1" >> /boot/config.txt
$ sudo pip3 install mh-z19
$ sudo reboot  #再起動
$ sudo python3 -m mh_z19
{"co2": 1155} #CO2濃度が出力されればOK
```

上記のように、CO2濃度が出力されればひとまず完成。




# Googleスプレッドシートへのデータ送信
ラズパイ上でPython実行により、Googleスプレッドシートへ測定値を送信していきます。
## 準備
以下の記事を参考にスプレッドシートの共有まで実行してください。
[PythonでGoogleスプレッドシートを編集](https://qiita.com/akabei/items/0eac37cb852ad476c6b9)



## コード
下記のpythonファイルを１分ごとに実行するためにcronへ登録。必要なライブラリは適宜pip installしてください。

```
# pip3 install gspread oauth2client
```

```python:co2_out.py
import subprocess
import json
import datetime
import gspread
from oauth2client.service_account import ServiceAccountCredentials

# MH-Z19から測定値を取得
args = ['python3', '-m', 'mh_z19']
res = subprocess.check_output(args).decode('utf-8')
res = json.loads(res)
co2_concentration = res['co2']
time_now = datetime.datetime.now()

scope = ['https://spreadsheets.google.com/feeds','https://www.googleapis.com/auth/drive']

# JSONファイルの中身をそのまま転記
credential = {
  "type": "service_account",
  "project_id": "xxxxxxxx",
  "private_key_id": "xxxxxxxxxxxxxxxxxxxxxx",
  "private_key": "-----BEGIN PRIVATE KEY-----\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "client_email": "xxxxxxx@xxxxxxx.iam.gserviceaccount.com",
  "client_id": "xxxxxxxxxxxxxxxxxxxxxxx",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/xxxxxxxxxxxxxxxx.iam.gserviceaccount.com"
}

# Googleスプレッドシートは測定値を追記
credentials = ServiceAccountCredentials.from_json_keyfile_dict(credential, scope)
gc = gspread.authorize(credentials)
wks = gc.open('homeCO2').sheet1
rowToAdd = [time_now.strftime('%Y/%m/%d %H:%M:%S'), co2_concentration]
wks.insert_row(rowToAdd) # グラフ描画のために一番上に測定値データを挿入（シートには昇順でデータが格納される）

````


```bash:crontab
・・・
* * * * * python3 /home/hogehoge/co2/co2_out.py  #crontabへ上記のpython実行を追加
```

下記の様に測定値データが反映されれば成功です。
<img width="286" alt="スクリーンショット 2019-12-08 10.23.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111324/1a829ce7-d10b-879f-0c43-9b63e36cb37b.png">

ここで、直近１日ごとのグラフを確認してみましょう。
同ファイルの別シートのA1〜B1440へ「=INDIRECT("シート1!"&address(row(),column()))」を記載してください。
<img width="608" alt="スクリーンショット 2019-12-08 10.26.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111324/938430ed-60f8-d90b-9fc7-3cb6abe0efd2.png">
これが直近１日（24時間×60分）分の測定値になります。
<img width="2439" alt="スクリーンショット 2019-12-08 14.09.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111324/5c5a9424-5cf0-c669-cc96-73f7831b1858.png">

図を見ても分かる通り、人が活動している間のCO2濃度は徐々に上昇し、換気をすると一気にCO2濃度が低下していることがわかります。
正直、CO2濃度の感度がここまで高いと思っておらず、換気の重要性を実感しました。



# LINE BOTによる通知システムの構築
ここからは、LINEが提供しているMessagingAPIを利用して一定のCO2濃度を（1200ppm）を超えたら換気を促す通知を出し、あるCO2濃度（500ppm)を下回ったら換気完了の通知を出すシステムを構築します。

## LINE公式アカウントの開設
すでにLINE公式アカウントの開設は完了しているため割愛しますが、下記が参考になると思います。
[LINE BOTの作り方を世界一わかりやすく解説（１）【アカウント準備編】](https://qiita.com/yoshizaki_kkgk/items/bd4277d3943200beab26)

## GASの設定
先程作成したスプレッドシートを開き、「ツール」→「スクリプトエディタ」を選択します。
<img width="326" alt="スクリーンショット 2019-12-08 13.48.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111324/87497b0c-93b7-6a1f-b616-a20ed2f46e79.png">
「無題のプロジェクト」部分を押下し、名前を記入します。
<img width="370" alt="スクリーンショット 2019-12-08 13.48.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111324/3b9aeeeb-da33-14a4-0840-d0ddc0d93995.png">
<img width="519" alt="スクリーンショット 2019-12-08 13.49.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111324/923fb45b-e2e5-20ef-bf25-f5649bec365b.png">
下記のjavascriptをコピーし、保存します。

```javascript:pushToLine.gs
function pushMessage() {
  const CHANNEL_ACCESS_TOKEN = "XXXXXXXXXXXXXXXXXXX"; 
  const USER_ID = [
    'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
    ];
  const VEBTILATION_CO2 = 1200; //これを超えたら換気を促す
  const CLOSE_WINDOW_CO2 = 600; //これを下回ったら換気終了を通知
  
//PUSH用メッセージのテンプレート
  var postData = {
    "to": [USER_ID[0],USER_ID[1]],
    "messages": [{
      "type": "text",
      "text": "",
    }]
  }
  
  //SpreadSheetの取得
  SS = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = SS.getSheetByName("シート1"); //Spreadsheetのシート名（タブ名）

  //二酸化炭素濃度を取得
  //過去5回の平均を取得
  var co2_1 = sheet.getRange("B1").getValue(); 
  var co2_2 = sheet.getRange("B2").getValue(); 
  var co2_3 = sheet.getRange("B3").getValue(); 
  var co2_4 = sheet.getRange("B4").getValue(); 
  var co2_5 = sheet.getRange("B5").getValue(); 
  
  var co2 = (co2_1 + co2_2 + co2_3 + co2_4 + co2_5) / 5;
  if(co2 > 3000){
    return;
  }
  Logger.log(co2);
    
  //1回の変更で2回GASが実行される
  //よくわからんけど2回のうち1回はエラーになる
  try{
    var cell = sheet.getActiveCell();
    console.log('%s %s',cell.getColumn(),cell.getRow());
  }catch(e){
    console.log('Out of Range Error.');
    return;
  }
   
  //ここらへんはチャタリング（測定値がブレることによって短時間で何回も通知が来る）の防止
  //過去30分間で通知をしたかどうか
  //Trueならば通知はしなくてもいい
  var is_notification_past30_min = false
  var sheetdata = sheet.getSheetValues(1, 3, 30, 1);   
  for(var i = 0; i<30; i++){
    if(sheetdata[i][0] == '要換気'){
      is_notification_past30_min = true;
      break;
    }
  }
  //直近の通知が「要換気」かどうか
  var is_notification_kanki = false
  for(var i = 1; i <= 1440; i++){
    cell = 'C' + i;
    var value =  sheet.getRange(cell).getValue();
    //要換気であれば換気終了の通知を出しても良い
    if(value == '要換気'){
      is_notification_kanki = true;
      break;
    //換気終了であればチャタリング防止
    }else if(value == '換気終了'){
      is_notification_kanki = false;
      break
    }  
  }
  

  //通知有無のフラグ 0:通知不要 1:換気通知 2:換気終了通知
  var is_notification_flag = 0;
  // 通知有無の判定
  if(co2 > VEBTILATION_CO2 && is_notification_past30_min == false){  //規定の濃度が超えてる かつ 直近30分以内に通知をしていない
    is_notification_flag = 1;  //CO2濃度が一定値を超えた場合
  }else if(co2 < CLOSE_WINDOW_CO2 && is_notification_kanki == true){  //規定の濃度を下回った かつ 直近の通知が「要換気」
    is_notification_flag = 2;  //CO2濃度が一定値を超えた場合
  }else{
    is_notification_flag = 0;
    console.log('NOT need to push a message');
  }

  // 通知有無の判定
  if(is_notification_flag == 1){
    postData.messages[0].text = "換気してください。：" + parseInt(co2) + "ppm";
    var range = sheet.getRange("C1").setValue('要換気');
  }else if(is_notification_flag == 2){
    postData.messages[0].text = "換気を完了してください。：" + parseInt(co2) + "ppm";
    var range = sheet.getRange("C1").setValue('換気終了');
  }else{
    return;
  }
  
  var url = 'https://api.line.me/v2/bot/message/multicast';
  var headers = {
    'Content-Type':'application/json; charset=UTF-8',
    'Authorization':'Bearer ' + CHANNEL_ACCESS_TOKEN
  };



  var options = {
    'method':'post',
    'headers':headers,
    'payload':JSON.stringify(postData)
  };
  //LINE messanger APIを叩く
  UrlFetchApp.fetch(url, options);
}


```

「公開」→「ウェブアプリケーションとして導入」を押下し、下記の設定で保存をします。
<img width="346" alt="スクリーンショット 2019-12-08 14.20.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111324/835cc74a-1a82-4b48-6a49-555b2ed24e03.png">

<img width="474" alt="68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3131313332342f37653661666264302d366230332d646135382d353338342d3730343736343466333039632e706e67.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111324/b049de92-486a-56f3-0d32-25b094d8884c.png">

初めて実施する場合には下記の様なエラーが出る場合がありますが、認証をすると使えるようになります。
<img width="618" alt="68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3131313332342f66656337373331362d613962322d353165312d633732632d3139303437656432613433302e706e67.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111324/fb100268-d806-4963-13e1-e81422b314fd.png">

払い出されたCallback URLをWebHookにコピーすれば完成です。
※Callback URLが有効になるまで時間がかかるようです。「検証する」を押下し、エラーが帰ってきてもしばらく待ってみましょう。
<img width="477" alt="68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3131313332342f65633333623734302d663038332d386534322d636164662d3763396237323732383766332e706e67.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111324/87480e63-3cfe-d74c-666c-15a76848470d.png">

<img width="1061" alt="68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3131313332342f64623834313564652d666332302d366263622d623434362d3837353534333930396365382e706e67.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111324/45572a2a-f022-b90b-60d8-add478befa8c.png">



## 実装結果
成功するとこのようになります。
<img width="375" alt="IMG_3853.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111324/bba58ee1-82b0-fcfe-aae4-f724cd78167f.png">

## まとめ
なんとなく頭がぼーっとするなーと思ったときに通知が飛んでくるのを見ると、酸素って大事。


# 参考
[[Google Apps Script] LINE Bot を作ってみよう](https://dev.classmethod.jp/etc/gas-line/)