リモートワークを快適にする換気システムを作ってみた

# はじめに
リモートワーク中、頭がぼーっとするときはありませんか？
そんなときはもしかすると、部屋の二酸化炭素濃度が高いかもしれません。

この記事では、Raspberry Pi（以降、RPi）、CO2センサ、Nature Remo 3、および Amazon Echo（以降、Echo）を用いて、リモートワークを快適にする換気システムを作る方法について、解説します。

この換気システムを使用すれば、**換気が必要なタイミング・換気が完了したタイミングを Echo に教えてもらうことができます**。

# 換気システムの全体像
![換気システムの全体像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242339/4bdba0df-d040-ee0d-ca6d-0bd7cf41da0b.png)
# 換気システムの機能

- **CO2センサから取得した二酸化炭素濃度の値、Nature Remo 3 から取得した温度・湿度・照度の値（以降、各センサの値）をスプレッドシートに記録**
- **スプレッドシートに記録した各センサの値をグラフ化し、Web に公開**
- **現在の各センサの値を Echo に教えてもらう**
- **換気が必要なタイミングを Echo に教えてもらう**
- **換気が完了したタイミングを Echo に教えてもらう**

# 使用機材
- **RPi：Raspberry Pi 3 Model B**
- **CO2センサ：MH-Z19B**
- **温度・湿度・照度センサ：Nature Remo 3**

:::note warn
温度・湿度・照度が必要なければ、Nature Remo 3 は用意しなくても大丈夫です。
:::


# 各機能の実装
## RPi に接続した CO2センサから二酸化炭素濃度を取得
### RPi と CO2センサをジャンパワイヤで接続
- **参考：[mh-z19](https://pypi.org/project/mh-z19/)**

### シリアル通信を有効化
- **`$ sudo raspi-config` を実行して下記の順番で画面を操作し、設定を変更**
  1. Interfacing Options
  2. Serial Port
  3. Would you like a login shell to be accessible over serial ? → `<No>`
  4. Would you like the serial port hardware to be enabled ? → `<Yes>`
  5. The serial login shell is disabled The serial interface is enabled → `<Ok>`
- **`$ sudo reboot` を実行し、設定を反映**

### 必要なライブラリをインストール
- **`$ pip install mh-z19` を実行**
- **参考：[mh-z19](https://pypi.org/project/mh-z19/)**

### 動作確認
- **下記の `get_co2.py` を実行**
  - `/home/pi/.virtualenvs/py3/bin/python` は適宜変更（例：`$ which python` の実行結果など）
  ```python: get_co2.py
    import subprocess
    import json


    def get_co2():
        cmd = ['sudo', '/home/pi/.virtualenvs/py3/bin/python', '-m', 'mh_z19']
        res = subprocess.check_output(cmd).decode()
        co2 = json.loads(res)['co2']

        return co2


    if __name__ == '__main__':
        co2 = get_co2()
        print(co2)
  ```

### CO2センサから取得される二酸化炭素濃度のキャリブレーション
- **下記を実行し、CO2センサを屋外（400ppm）に20分間放置**
  ```
    $ python
    >>> import mh_z19
    >>> mh_z19.zero_point_calibration()
  ```

---

## NatureRemoAPI を叩いて Nature Remo 3 から温度・湿度・照度を取得
### 必要なライブラリをインストール
- **参考：[Nature Remo を Python から操作する](https://qiita.com/morinokami/items/6eb2ac6bed48d2c7534b)**
- **`$ pip install nature-remo` を実行**

### NatureRemoAPI を利用するためのアクセストークンを発行
- **[Nature](https://home.nature.global/) にアクセスし、アクセストークンを発行**

### 動作確認
- **下記の `get_natureremo_data.py` を実行**
  ```python: get_natureremo_data.py
    from remo import NatureRemoAPI
    import json


    def get_natureremo_data():
        api = NatureRemoAPI(json.load(open('./config/setting.json'))['natureremo_token'])
        device = api.get_devices()[0]
        te = device.newest_events['te'].val  # 温度
        hu = device.newest_events['hu'].val  # 湿度
        il = device.newest_events['il'].val  # 照度

        return device, te, hu, il

    if __name__ == '__main__':
        device, te, hu, il = get_natureremo_data()
        print(device)
        print(te, hu, il)
  ```
  ```json: setting.json
    {
        "natureremo_token": "**********"
    }
  ```
---

## 各センサの値を Google Apps Script を用いてスプレッドシートに記録
### 各センサの値を記録・グラフ化するためのスプレッドシートを作成
- 参考：[Raspberry PiからGoogleスプレッドシートにCO2濃度データを保存する](https://blog.ko31.com/202004/raspberry-pi-to-google-spreadsheet/)
- **シートの追加**
  - `log`
    - センサデータの記録用
    - A列：時刻、B列：二酸化炭素濃度、C列：温度、D列：湿度、E列：照度
    - 完成後の表示例：

        |  | A | B | C | D | E |
        | --- | --- | --- | --- | --- | --- |
        | 1 | 2022/04/12 8:51:59 | 559 | 26.6 | 51 | 137 |
  - `filter`
      - `log` から最新900件（5時間分）の各センサの値を取得
      - A1 に下記を記入
        ```
        =query(
          log!A:E,
          "SELECT A, B, C, D, E ORDER BY A DESC LIMIT 900 FORMAT A 'hh:mm'"
        )
        ```
      - 完成後の表示例：

        |  | A | B | C | D | E |
        | --- | --- | --- | --- | --- | --- |
        | 1 | 08:51 | 559 | 26.6 | 51 | 137 |
  - `graph`
    - `filter` を用いて各センサの値をグラフ化
    - A1 に下記を記入
      ```
      =CONCATENATE(
        TO_TEXT(filter!A:A), ", ",
        TO_TEXT(filter!B:B), "ppm, ",
        TO_TEXT(filter!C:C), "℃, ",
        TO_TEXT(filter!D:D), "%, ",
        TO_TEXT(filter!E:E), "lx "
      )
      ```
    - A2 にグラフを挿入
      - グラフをクリックして、グラフエディタを編集
        - 設定
          - 以下の画像を参考に各項目を記入
          - データ範囲：`filter!A1:A900,filter!B1:B900,filter!C1:C900,filter!D1:D900,filter!E1:E900`
            <img width="300" alt="グラフエディタ_設定" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242339/4acd71e4-d2c8-d2fd-5fe5-c875b1a56c73.png">
        - カスタマイズ
          - 横軸の`軸ラベルの順序を逆にする`にチェック
          - 好みのデザインにカスタマイズ
    - 完成後の表示例：
      <img width="300" alt="グラフ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242339/7366e6dc-8182-ad46-6caf-de6b43c4dfa0.png">


### 各センサの値をスプレッドシートへ記録する方法
- **GAS（Google Apps Script）を利用**
  - GAS は、作成したプログラムを Webアプリとして公開可能
- **記録方法の流れ**
  - RPi が GAS にセンサデータを POST
  - GAS が各センサの値をスプレッドシートに記録

### RPi から POSTされた各センサの値を受け取り、スプレッドシートへ記録する GAS のプログラムを作成
- **参考：[Raspberry PiからGoogleスプレッドシートにCO2濃度データを保存する](https://blog.ko31.com/202004/raspberry-pi-to-google-spreadsheet/#co-index-0)**
- **GAS のプログラムを書くためのエディタの開き方**
    - スプレッドシートを開く
    - 拡張機能、Apps Script の順にクリック
- **GAS のプログラム（`sensor_data.gs`）を作成**
    ```javascript: sensor_data.gs
    function doPost(e) {
        // パラメータの値を取得
        var co2 = e.parameter.co2;  // co2
        var te = e.parameter.te;  // 温度
        var hu = e.parameter.hu;  // 湿度
        var il = e.parameter.il;  // 照度

        // 保存先のシートを取得
        var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('log');

        // シートに各センサの値を追記
        sheet.appendRow([new Date(), co2, te, hu, il]);
    }
    ```

### GAS のプログラムを Webアプリとして公開
- **デプロイ、新しいデプロイの順にクリック**
    - 設定する項目
        - 新しい説明文：任意
        - 次のユーザとして実行：自分
        - アクセスできるユーザ：全員
    - 許可を確認を選択し、自分のアカウントをクリック
- **「このアプリは確認されていません」の画面における操作**
    - 左下の詳細をクリック
    - （安全ではないページ）に移動をクリック
    - 許可をクリック
- **Webアプリの URL をコピー**


### RPi が GAS にセンサデータを POSTするプログラムの作成
```python logging_sensor_data.py
import schedule
import time
from get_co2 import get_co2
from get_natureremo_data import get_natureremo_data
import requests
import json


def logging_sensor_data():
    co2 = get_co2()
    device, te, hu, il = get_natureremo_data()

    response = requests.post(
        url=json.load(open('./config/setting.json'))['apps_script_url'],
        data={
            'co2': f'{co2}',
            'te': f'{te}',
            'hu': f'{hu}',
            'il': f'{il}'
        }
    )


def main():
    schedule.every(1).minutes.do(logging_sensor_data)

    while True:
        schedule.run_pending()
        time.sleep(1)


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

```json: setting.json
{
    "apps_script_url": "**********"
}
```

### 動作確認
- **上記の `logging_sensor_data.py` を実行**
- **スプレッドシートが更新されるか確認**

### スプレッドシートの `graph` を Web に公開
- **ファイル、Web に公開の順にクリック**

---

## Voiceflow で Alexaスキルを作成し、現在の各センサの値を Echo に教えてもらう
### プロジェクトを作成
- **参考：[Raspberry Piで測定したCO2濃度をAlexaが話せるよう連携する
](https://blog.ko31.com/202005/raspberry-pi-send-co2-to-alexa/)**
- **[Voiceflow](https://www.voiceflow.com/) のアカウントを作成**
- **Create Project をクリックしてプロジェクト名を入力し、Japanese を選択**

### Alexaスキルを作成
- **以下の画像を参考に、Steps から必要なカードを選択して配置**
  <img width="700" alt="Voiceflow_steps" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242339/93cc8ee6-8fad-ea7e-ae9d-1f9aeb387d88.png">
  - Speak カード
    - `Speak sensor data`
    - `Speak err msg`
  - Google Sheets カード
    - `Get sensor data`
- **現在の各センサの値を取得するためのカード（`Get sensor data`）を編集**
  - Retrieve Data を選択
  - ユーザ、スプレッドシート、シート（`filter`）を選択
  - 以下の画像を参考に、With Settings と Mapping Output を編集
    <img width="400" alt="With_Settings" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242339/eb20dd5e-07af-da3c-7546-aaca9a3cf147.png">
    <img width="400" alt="Mapping_Output" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242339/d58db674-4e5c-510a-465f-1cd45a6209ce.png">

- **Start カードをクリックしてテストを実行し、動作を確認**

### Alexaスキルをデプロイ
- **右上の Upload to Alexa をクリックし、その後、Connect Amazon をクリック**
- **Alexa開発者コンソールにログインし、Alexaシミュレータで Alexaスキルの動作を確認**
  - Alexaシミュレータにセンサーと入力

### 動作確認
- **Echo に「エコー、センサー」と話しかけてみる**
  - 応答例：現在の時刻は14時25分、二酸化炭素濃度は583ppm、温度は26℃、湿度は39%、光量は28lxです。

### おまけ
- **私の場合、この機能は定型アクションに組み込んで使っていたりします。**
    <img width="300" alt="定型アクション" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242339/fb0346aa-ab22-4e5e-0ce0-d4340ecf805e.png">

---

## alexa-remote-control を用い、換気が必要なタイミングを Echo に教えてもらう
### 換気が必要なタイミング
- **CO2センサから取得される二酸化炭素濃度の値が1000を超えた場合**
- **参考：[CO2モニター（二酸化炭素計測機）](https://www.mhi-mth.co.jp/business/proposal/06/)**

### alexa-remote-control の設定
- **必要なライブラリをインストール**
  - `$ sudo apt install jq` を実行

- **alexa-remote-control をダウンロード**
  - `$ wget https://raw.githubusercontent.com/thorsten-gehrig/alexa-remote-control/master/alexa_remote_control.sh` を実行

- **`alexa_remote_control.sh` を編集**
  - 編集項目
    ```
    SET_EMAIL='**********'
    SET_PASSWORD='**********'
    SET_LANGUAGE='ja-JP'
    SET_TTS_LOCALE='ja-JP'
    SET_AMAZON='amazon.co.jp'
    SET_ALEXA='alexa.amazon.co.jp'
    ```

- **`alexa_remote_control.sh` の権限を変更**
  - `$ sudo chmod 777 alexa_remote_control.sh` を実行

### おまけ（LINEへ通知するための設定）
- **[LINE Notify](https://notify-bot.line.me/ja/) にアクセス**
- **トークンを発行する、1:1でLINE Notifyから通知を受け取るの順にクリック**

### 動作確認
- **下記の `notify.py` を実行**
    ```python: notify.py
    import subprocess
    import json
    import requests


    def let_echo_speak(msg):
        cmd = ['./sh/alexa_remote_control.sh', '-e', f'speak:{msg}']
        res = subprocess.check_output(cmd).decode()
        send_line_notify(msg=msg)


    def send_line_notify(msg):
        line_notify_token = json.load(open('./config/setting.json'))['line_notify_token']
        line_notify_api = 'https://notify-api.line.me/api/notify'
        headers = {'Authorization': f'Bearer {line_notify_token}'}
        data = {'message': msg}
        requests.post(line_notify_api, headers=headers, data=data)


    if __name__ == '__main__':
        let_echo_speak(msg='テスト')
    ```
    ```json: setting.json
    {
        "line_notify_token": "**********"
    }
    ```
---

## Node-RED を用いて RPi と Echo を連携し、換気が完了したタイミングを Echo に教えてもらう
### 換気が完了したタイミング
- **Echo に「換気 ON」と伝えてから、初めてCO2センサから取得される二酸化炭素濃度の値が500を下回った場合**

### Node-RED の起動
- **参考：[Raspberry Piで実行する](https://nodered.jp/docs/getting-started/raspberrypi)**
- **Node-RED のスクリプトをダウンロード**
  - `$ bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)` を実行
- **Node-RED のサービスの起動時実行を有効化**
  - `$ sudo systemctl enable nodered.service` を実行
- **Node-RED のサービスを起動**
  - `$ node-red-start` を実行
- **ログに表示される URL（例：http://192.168.10.108:1880/）に PC からアクセス**

### Node-RED を用いて RPi と Echo を連携
- **Node-RED Alexa Home Skill Bridge の設定**
  - [Node-RED Alexa Home Skill Bridge](https://alexa-node-red.bm.hardill.me.uk/) にアクセスし、Node-RED Alexa Home Skill Bridge のアカウントを作成
  - Devices、Add Device の順にクリックし、下記の項目を設定
    ```
    Name：換気
    Description：換気システム
    Actions：On, Off
    Application Type：SWITCH
    ```
- **Node-RED に Node-RED Alexa Home Skill Bridge を追加**
    - 参考：[Node-RedでAlexa Home Skill Bridgeを設定する](https://qiita.com/loach/items/782dec67962e4085e387)
    - 右上のタブ、パレットの管理、ノードを追加の順にクリック
    - 検索欄に `node-red-contrib-alexa-home-skill` と入力し、ノードを追加をクリック
- **alexa-home ノードの設定**
    - alexa-home ノードの設定を配置し、ダブルクリック
    - Acount の右の編集ボタンをクリックし、Node-RED Alexa Home Skill Bredge のサイトのユーザー名とパスワードを入力して add をクリック
- **Node-RED の Alexaスキルを有効化**
  - [Node-RED](https://www.amazon.co.jp/Ben-Hardill-Node-RED/dp/B01N0D97FZ) にアクセス
  - 端末を検出をクリック

### Node-RED のフローを作成
- **以下の画像を参考に、ノードを配置**
  <img width="800" alt="Node-RED のフロー" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242339/22ec80ec-6b04-b949-6b5b-13a602d34cf4.png">
  - switch ノード
      - is-true
          - http request ノード
              - メソッド：GET
              - URL：http://localhost:50080/ventilate
      - if-false
          - http request ノード
              - メソッド：GET
              - URL：http://localhost:50080/ventilate/stop
  - debug ノード
- **Node-RED のフローのデプロイ**
  - 右上のデプロイをクリック

### 動作確認
- **下記の `server.py` を実行**
```python: server.py
from flask import Flask, Response
import json
from notify import let_echo_speak
from get_co2 import get_co2
import time

app = Flask(__name__)
ventilation = False


@app.route('/ventilate')
def ventilate():
    global ventilation
    ventilation = True
    check_ventilation_is_complete()
    return Response(response=json.dumps({'message': 'Start Ventilation!'}), status=200)


@app.route('/stop')
def stop():
    global ventilation
    ventilation = False
    return Response(response=json.dumps({'message': 'Stop Ventilation!'}), status=200)


def check_ventilation_is_complete():
    let_echo_speak(msg='換気を開始します。')

    while True:
        co2 = get_co2()
        if int(co2) <= 500:
            let_echo_speak(msg='換気が完了しました。')
            break
        elif not ventilation:
            let_echo_speak(msg='換気を終了します。')
            break
        else:
            time.sleep(60)


def main():
    app.run(host='0.0.0.0', port=50080)


if __name__ == '__main__':
    main()
```
- **Echo に「換気ON」と話しかけてみて、Echo から「換気を開始します」と応答があるか確認**

---

## RPi の低消費電力化（Wi-Fi 以外の不要な機能を停止）
### オンボードLED の無効化（RPi Model 3B の場合）
- **`/boot/config.txt` に下記を追記後、再起動**
  - Act LED：データアクセス時に緑色に点滅
  - Pwr LED：起動後常に赤色に点灯
    ```shell: config.txt
    # Disable the Activity LED
    dtparam=act_led_trigger=none,act_led_activelow=on
    # Disable the PWR LED
    dtparam=pwr_led_trigger=none,pwr_led_activelow=on
    ```

### HDMI の無効化
- **`/etc/rc.local` に下記を追記後、再起動**
    ```shell: rc.loal
    # Disable HDMI
    tvservice --off
    ```

### Bluetooth の無効化（RPi Model 3B の場合）
- **`/boot/config.txt` に下記を追記後、再起動**
    ```shell: config.txt
    # Disable Bluetooth
    toverlay=disable-bt
    ```

### USBコントローラの無効化
- **`/etc/rc.local` に下記を追記後、再起動**
    ```shell: rc.loal
    # Disable USB
    echo '1-1' |sudo tee /sys/bus/usb/drivers/usb/unbind
    ```

---

# おわりに
この記事では、RPi、CO2センサ、Nature Remo 3、および Echo を用いて、リモートワークを快適にする換気システムを作る方法について解説しました。

皆さんのリモートワークが、少しでも快適になればと思います。