在宅の環境を少しでもよくしたい...そうだ、部屋の空気に注目してみよう！

# はじめに
エイチームフィナジーアドベントカレンダー12日目を担当します @yothio です。

某ウイルスの影響により、エイチームグループでは2020年の3月頃から在宅でも業務ができるようになりました。
ただ、今までのオフィス勤務と同じぐらいパフォーマンスをだすのはなかなか難しいと思ってます、大きく変化がある「職場環境」「コミュニケーション」の2つによってパフォーマンスが発揮しづらいかと思います。

自宅でもパフォーマンスを落とさず、むしろ上げるために意識して行ってきたことをシステム化して話をします。

内容としては「**部屋の二酸化炭素濃度**」を「**計測**」することで
「**換気を促し**」「**在宅の環境を良くすること**」です

## 二酸化炭素とパフォーマンスの関係
 そもそも「二酸化炭素濃度とパフォーマンスにどんな関係があるんだ？」と思う方が多いと思います
下記に掲載しているリンクのどれもが「ローレンス・バークレー国立研究所」の結果を使用した記事となっていますが二酸化炭素（CO2）濃度が増えることによってパフォーマンスに悪影響を与えていることがわかったようです。
> CO2濃度が脳の機能（意思決定）に与える影響についてニューヨーク州立大学と共同研究を行いました。22人の被験者に対し9項目の意思決定テストを行い比較したところ、CO2濃度が600ppmの環境に比べて、1000ppmで6項目が劣り、2500ppmの環境では7項目が大幅に劣ったそうです。

[上記引用元](https://studyhacker.net/columns/kanki-performance-up)
[参考リンクA](https://www.daringdaddy.com/entry/2018/01/03/%E3%82%AA%E3%83%95%E3%82%A3%E3%82%B9%E3%81%AE%E4%BA%8C%E9%85%B8%E5%8C%96%E7%82%AD%E7%B4%A0%E6%BF%83%E5%BA%A6%EF%BC%88CO2%E6%BF%83%E5%BA%A6%EF%BC%89%E3%81%8C%E3%80%81%E3%83%91%E3%83%95%E3%82%A9%E3%83%BC)
[参考リンクB](https://attendant.hataraba.com/office-layout-27/)

また、換気扇を用いた話ですが下記のサイトでも「なぜ換気を行うのか」「換気を行うことでの良い効果」について書かれています
[なんで換気をするのかしら？｜換気の情報サイト「いいね！換気扇」](https://www.kanki-day.jp/mama/mama_01.html)

そのことを踏まえると定期的な換気を行うのはとても大事だということがわかります。

## システム概要
二酸化炭素の濃度を測るモジュールとして[MH-Z14A](https://www.amazon.co.jp/Delaman-CO2%E3%82%BB%E3%83%B3%E3%82%B5%E3%83%BC%E3%80%81NDIR%E4%BA%8C%E9%85%B8%E5%8C%96%E7%82%AD%E7%B4%A0%E3%82%BB%E3%83%B3%E3%82%B5%E3%83%BC%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%80%81PWM%E5%87%BA%E5%8A%9B%E3%80%81MH-Z14A-%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%AB%E3%83%9D%E3%83%BC%E3%83%88%E3%80%81%E4%BA%8C%E9%85%B8%E5%8C%96%E7%82%AD%E7%B4%A0-%E6%A4%9C%E5%87%BA/dp/B07QB7M5ZX/)を使用し
モジュールと接続し、通知までのつなぎとして[Raspberry Pi 3 Model B](https://www.amazon.co.jp/RASPBERRY-PI-TSI-PI018-Clear-Raspberry-Pi3-Model/dp/B01D1FR29M)(以下: RaspberryPi)
そして通知先に[Slack](https://slack.com/intl/ja-jp/)を使用します

<img width=500 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199819/93b895a9-ca45-3c16-7ea5-eb3fd69bb983.png />


流れとしては下記のとおりです
1. RaspberryPiのセットアップ
2. MH-Z14Aと繋ぐ
3. 二酸化炭素濃度を取得するプログラムの記述
4. 通知を飛ばすためのしきい値設定
5. Slackへの通知設定

### RaspberryPiのセットアップ
初期セットアップについては他の方が記事にしているのでザックリと実施したことを載せていきます
今回必ず実施する必要があるのは一番下のUARTの有効化です

- [RaspberryPiのOSを書き込み]
(https://qiita.com/shippokun/items/9070fc58f69d8c063e44)
    - OSないと始まらないので、Raspbianを選択
- [買ったらまず実施！RaspberryPiのセキュリティ対策 \- Qiita](https://qiita.com/c60evaporator/items/ebe9c6e8a445fed859dc)
    - デフォルトユーザー使うのよくないって広辞苑に書いてあった。
- [WiFi設定\(コマンドライン\) － Raspberry Pi公式ドキュメントを日本語訳](http://igarashi-systems.com/sample/translation/raspberry-pi/configuration/wireless-cli.html)
- [Raspberry Pi に固定IPアドレスを割り当てる方法（Raspbian Jessie） \- Qiita](https://qiita.com/marie_khr/items/b088ffb252a92eee8f5d)
    - SSHで接続するのにIPが変わると嬉しくないので実施
- [Raspberry PiでUARTの有効化＋シリアル通信 \- Qiita](https://qiita.com/s_fujii/items/466d455ca19fb4c20744)
    - モジュールとのやりとりはUARTで実施するため

### MH-Z14Aと繋ぐ
[コチラ](https://www.winsen-sensor.com/d/files/MH-Z14A.pdf)がMHZ-14Aのマニュアルになります。

<img width=500 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199819/773e7fe9-c519-eec6-939a-fbebe34ccaa0.png />

とても丁寧にPIN番号と説明が書いてあるのでRaspberryPiのGPIOとにらめっこしながら繋げていきます。

||RaspberryPi|MH-Z14A|
|---|---|---|
|電源|4|17|
|GND|5|16|
|入力ピン|8|18|
|出力ピン|10|19|

対応したPINを設定した写真がこちらになります

<img width=500 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199819/1c58aa72-1602-b606-4223-181083c07e73.png />

<img width=500 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199819/ea05b281-e260-4202-294a-7ee8e148e9d9.png />
[GPIO \- Raspberry Pi Documentation](https://www.raspberrypi.org/documentation/usage/gpio/README.md)

### 二酸化炭素濃度を取得する
次は実際にコードを書いて二酸化炭素濃度を取得してみます
pythonでシリアル通信の結果を取得するため、あらかじめ`pyserial`をインストールしておきます。

```bash
$ pip install pyserial
```

MH-Z14Aに関すること調べていると動きそうなコードがあったため、少し変更して使用いたします
[GitHub \- keepworking/MH\-Z14A\-PI: co2 sensor python code](https://github.com/keepworking/MH-Z14A-PI)

```py:mh_z14a.py
import serial
import time
import datetime
import json
import argparse
import sys
import csv
import requests

class MHZ14A():
    PACKET = [0xff, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79]
    ZERO = [0xff, 0x01, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78]

    def __init__(self, ser):
        self.serial = serial.Serial(ser, 9600, timeout=3)
        time.sleep(2)

    def zero(self):
        self.serial.write(bytearray(MHZ14A.ZERO))

    def get(self):
        self.serial.write(bytearray(MHZ14A.PACKET))
        res = self.serial.read(size=9)
        res = bytearray(res)
        checksum = 0xff & (~(res[1] + res[2] + res[3] + res[4] + res[5] + res[6] + res[7]) + 1)
        if res[8] == checksum:
            return {
                "date_label": datetime.datetime.today().strftime("%Y/%m/%d %H:%M"),
                "ppm": (res[2] << 8) | res[3],
            }
        else:
            raise Exception("checksum: " + hex(checksum))

    def close(self):
        self.serial.close()

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--mode")
    args = parser.parse_args()
    mode = args.mode or "co2"

    co2 = MHZ14A("/dev/ttyS0")

    if mode == "zero":
        co2.zero()
    else:
        try:
            print(co2.get())
        except Exception as e:
            print(e)
            pass
    co2.close()

if __name__ == '__main__':
    main()
```

しっかりと二酸化炭素濃度（ppm）が取得できていますね
<img width=600 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199819/6e0deb1f-88cb-6078-7685-a1fc5e34571e.png />


### 通知を飛ばすためのしきい値設定
測った値を[Chart.js](https://www.chartjs.org/)を使用してグラフとして見ていきましょう。

<img width=600 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199819/08cf1d75-871d-21ad-39b7-f5f4a39685ed.png />
グラフを見る感じだと急激に数値が下がっている箇所はしっかり換気を実施した箇所となるため400ppmあたりが下限値ですね

また[気象庁](https://ds.data.jma.go.jp/ghg/kanshi/ghgp/co2_trend.html)が出している二酸化炭素濃度の経年変化を見る感じ下限値とほぼ同じなので正しく取得できていますね

今回測定してみて定期的に換気を実施していることもあり、1000ppmを超えることはなさそうなので800ppmをしきい値として設定しましょう。

### Slackへの通知設定
Slackへの通知はWebhookを利用して送信します
[Slack での Incoming Webhook の利用 \| Slack](https://slack.com/intl/ja-jp/help/articles/115005265063-Slack-%E3%81%A7%E3%81%AE-Incoming-Webhook-%E3%81%AE%E5%88%A9%E7%94%A8)の手順通りに実施すればWebhookのURLが手に入ります

上記リンクをクリックしたあとに下の画像にある「新しいSlackアプリを作成」をクリック

<img width=500 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199819/b292e9ed-779d-7944-3604-231bb17a14a1.png />

Slackへ通知を送るためのアプリを作成する
好きなApp Nameと通知を送信したいWorkspaceを選択

<img width=500 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199819/2897aa82-4e71-0ce7-4fc7-867da4b791c3.png />

---

さきほど作成したものが確認できるのでそれをクリック（今回はco2_notify）
<img width=500 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199819/112cecaf-84ef-be27-77e5-91c9fc5bd1e9.png />

---
Webhook経由で通知を送るため下記の赤枠の箇所をクリック

<img width=500 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199819/98afee5f-f2c7-a17c-01ab-ff5b00669fa1.png />

---
そしてWebhookの有効化
<img width=500 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199819/c4acb3a5-9dd7-8300-672c-f388c68c5562.png />

<img width=500 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199819/6326af05-d4c1-26b2-c13e-74b392f65ded.png />

---
あとは通知を飛ばしたいチャンネルを選択して、WebhookのURLをコピーしておく

<img width=500 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199819/aab932df-078d-a134-9a6f-bbc8121af642.png />

<img width=500 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199819/6763c0b8-776c-0704-4e7a-eab104d19b27.png />

あとはしきい値を超えたらWebhookのURLに対してリクエストを飛ばすように設定して完了

```py:mh_z14a.py
import requests

def notify_slack():
    headers = { 'Content-type': 'application/json' }
    data = '{"text":"二酸化炭素の濃度が濃くなっています、換気しましょう。"}'.encode("utf-8")
    url = 'さっきコピーしたWebhookのURL'
    requests.post(url, headers=headers, data=data)

def main():
    # ~~~省略~~~
    if mode == "zero":
        co2.zero()
    else:
        try:
            result = co2.get()
            if result.get('ppm') > 800:
                notify_slack()
```

<img width=600 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199819/1fc70999-0630-d656-16eb-2b35ca0da198.png />

素敵。

## 最後に
いかがだったでしょうか、某ウイルスの影響により在宅で勤務している方が多いので
そういった人たちに換気の重要性と自分なりの改善手段を伝えることができたなら幸いです

## プログラム全文

```py

import serial
import time
import datetime
import json
import argparse
import sys
import csv
import requests


class MHZ14A():
    PACKET = [0xff, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79]
    ZERO = [0xff, 0x01, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78]

    def __init__(self, ser):
        self.serial = serial.Serial(ser, 9600, timeout=3)
        time.sleep(2)

    def zero(self):
        self.serial.write(bytearray(MHZ14A.ZERO))

    def get(self):
        self.serial.write(bytearray(MHZ14A.PACKET))
        res = self.serial.read(size=9)
        res1 = self.serial.read()
        res = bytearray(res)
        checksum = 0xff & (~(res[1] + res[2] + res[3] + res[4] + res[5] + res[6] + res[7]) + 1)
        if res[8] == checksum:
            return {
                "date_label": datetime.datetime.today().strftime("%Y/%m/%d %H:%M"),
                "ppm": (res[2] << 8) | res[3],
                "dt": datetime.datetime.today().isoformat(),
                "ts": datetime.datetime.today().timestamp(),
            }
        else:
            raise Exception("checksum: " + hex(checksum))

    def close(self):
        self.serial.close()

def write_csv(params, output_path):
    with open(output_path + '/data.csv', 'a') as csv_file:

        fieldnames = ['ppm', 'date_label', 'dt', 'ts']
        writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
        writer.writerow(params)

def notify_slack(ppm):
    headers = {
                'Content-type': 'application/json',
                }
    data = '{"text":"二酸化炭素の濃度が濃くなっています、換気しましょう。"}'.encode("utf-8")
    url = 'hogehoge'
    requests.post(url, headers=headers, data=data)

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--mode")
    args = parser.parse_args()
    mode = args.mode or "co2"

    co2 = MHZ14A("/dev/ttyS0")

    if mode == "zero":
        co2.zero()
    else:
        try:
            result = co2.get()
            write_csv(result, '/home/Ryoshiki/workspace')
            if result.get('ppm') > 800:
                notify_slack(result.get('ppm'))
        except Exception as e:
            print(e)
            pass
    co2.close()


if __name__ == '__main__':
    main()

```