テレワークを安全・快適にするために、CO2濃度をTypetalkにお知らせするボットを自作してみた

こんにちは!管理部インハウス課の河内です。
早いもので、ヌーラボに入社して既に半年以上が経ちました。フルリモートでの働き方にもすっかり慣れてきた今日この頃ですが、気になってきたのが仕事部屋の空調管理です。特にCO2濃度は身体で直に感じることができないだけに、知らない間に高くなってしまっているかもしれません。放置していると眠くなったり集中力が低下したりと業務にも影響が出てきそうですし、何より健康によくありません。定期的に窓を開け閉めするのも仕事に熱中しているとついうっかり・・・となりがちです。そこで、ヌーラバーらしくDIY精神を発揮して、CO2濃度を教えてくれるシステムを自作してみることにしました。

はじめに

テレワークでの作業環境は、厚生労働省からガイドラインやチェックリストが公開されています。事務所衛生基準規則などの法規制は適用されないようですが、これらの基準と同等の作業環境になるよう改善を図ることが重要と書かれています。

テレワークの適切な導入及び実施の推進のためのガイドライン(厚生労働省)

(3) 自宅等でテレワークを行う際の作業環境整備の留意点
 テレワークを行う作業場が、労働者の自宅等事業者が業務のために提供している作業場以外である場合には、事務所衛生基準規則(昭和47年労働省令第43号)、労働安全衛生規則(一部、労働者を就業させる建設物その他の作業場に係る規定)及び「情報機器作業における労働衛生管理のためのガイドライン」(令和元年7月12日基発0712第3号)は一般には適用されないが、安全衛生に配慮したテレワークが実施されるよう、これらの衛生基準と同等の作業環境となるよう、事業者はテレワークを行う労働者に教育・助言等を行い、別紙2の「自宅等においてテレワークを行う際の作業環境を確認するためのチェックリスト(労働者用)」を活用すること等により、自宅等の作業環境に関する状況の報告を求めるとともに、必要な場合には、労使が協力して改善を図る又は自宅以外の場所(サテライトオフィス等)の活用を検討することが重要である。

自宅等においてテレワークを行う際の作業環境を確認するためのチェックリスト【労働者用】(厚生労働省)

2 作業環境の明るさや温度等について
(2) 作業の際に、窓の開閉や換気設備の活用により、空気の入れ換えを行っているか。

システム構成

Raspberry Pi にCO2センサーと温湿度センサーを接続し、取得したデータをGoogle Apps Script (GAS) で作成したWebアプリケーションに送ります。送られたデータはGoogleスプレッドシートに記録し、CO2濃度が閾値を超えた場合にTypetalkに通知します。
さらに、季節や時間帯による値の変化も確認したいのでデータをクラウド上に蓄積してダッシュボードで確認できるようにしてみました。

Diagramシステム構成

今回はデバイス購入にお金がかかるため、ソフトウェアはすべて無料で利用できるサービスを選ぶことにしました。

Raspberry Pi側

  • Raspberry Pi OS
  • Python 3.8.10

クラウド側

  • Googleスプレッドシート(CO2濃度および温湿度の記録)
  • Google Apps Script(センサーデータのリクエスト受け付け)
  • Google データポータル(データ可視化のためのダッシュボード)
  • Typetalk フリープラン(ボットによるユーザーへの通知)

システム要件

システム要件を検討します。

  • 換気を促すため、CO2濃度が閾値を超えたらTypetalkに通知します。
  • 窓を閉めるタイミングを掴みやすいように、CO2濃度が閾値を下回ったときもTypetalkに通知します。
  • CO2濃度の閾値はスプレッドシートで設定できるようにします。
  • 熱中症を防ぐため、部屋の温度と湿度も合わせて通知します。
  • 温度、湿度、CO2濃度を10分おきに Googleスプレッドシートに記録します。
  • 環境の変化を後で確認できるようにするため、スプレッドシートに記録したデータをグラフ化してダッシュボードに表示します。

ハードウェアの組み立て

作業環境を測定するための機器を組み立てます。以下のものを揃えました。Amazonなどで買えます。

  • Raspberry Pi 4 Model B 8GB
  • 温湿度センサー DHT22
  • CO2センサーモジュール MH-Z19C
  • ブレッドボードとジャンブワイヤ

CO2センサーにはいくつかの方式があるのですが、今回は外部環境の影響を受けにくいとされているNDIR方式のものを採用しました。
配線図はこのようになります。

Circuit Diagram配線図

実際に配線したものがこちらです。CO2センサーと温湿度センサーはブレッドボードに配置し、ジャンプワイヤで繋ぎます。CO2センサーとブレッドボードのピン間隔が微妙に合わなかったので、ブレッドボードを2枚用意しまたぐように配置しました。

System

ソフトウェアの開発(Raspberry Pi)

必要なライブラリ

温湿度センサーから値を取り出すために Adafruit_CircuitPython_DHT ライブラリを使用しました。
こちらのページを参考に、ダウンロード、インストールを行います。
https://circuitpython.readthedocs.io/projects/dht/en/latest/index.html#

$ pip install adafruit-circuitpython-dht

CO2センサーから値を取り出すために、mh-z19ライブラリを使用しました。
こちらのページを参考にインストールします。
https://github.com/UedaTakeyuki/mh-z19

$ pip install mh-z19

GASにPOSTリクエストを送るため、requests を使用します。

$ pip install requests

CO2濃度の測定

CO2濃度を測定するコードを書きます。
mh_z19.read()の戻り値から、CO2濃度を取り出します。

# co2.py
import mh_z19

def get_co2():
    r = mh_z19.read()
    return r['co2']

温度と湿度の測定

温度と湿度を測定するコードを書きます。
動作を確認したところ、センサーがうまく値を取得できずエラーになることがあったので、10回呼び出しを行い中央値を採用するようにしました。
今回使用した温湿度センサーDHT22はサンプリング周期が2秒以上のため、2秒間隔でループします。

# hygrothermo.py
import time
import board
import adafruit_dht
import statistics
import math
import sys
    
def get_hygrothermo():
    dhtDevice = adafruit_dht.DHT22(board.D23)
    temperature = []
    humidity = []
    INTERVAL = 2.0
    
    for i in range(10):
        try:
            temperature.append(dhtDevice.temperature)
            humidity.append(dhtDevice.humidity)
    
        except RuntimeError as error:
            print(error.args[0], file=sys.stderr)
            time.sleep(2.0)
            continue
        except Exception as error:
            dhtDevice.exit()
            raise error
    
        time.sleep(INTERVAL)
    
    # 中央値を求める
    temperature_median = statistics.median(temperature)
    humidity_median = statistics.median(humidity)

    return temperature_median, humidity_median

GASにPOSTリクエストを送る

取り出したCO2濃度と温湿度をJSON形式にして、GASにPOSTリクエストを送ります。GASのURLは、今回は環境変数 ECOMONITOR_URL から取得することにしました。

# ecomonitor.py
import json
import requests
import datetime
import os
from hygrothermo import get_hygrothermo
from co2 import get_co2
    
def make_json(temperature, humidity, co2):
    dt = datetime.datetime.now()
    date = dt.isoformat()
    
    return json.dumps({
        "date":date,
        "temperature": temperature,
        "humidity": humidity,
        "co2": co2
    })
    
def post_data():
    
    # 環境変数 ECOMONITOR_URL からGAS WebアプリケーションのURLを取り出す。
    url = os.getenv('ECOMONITOR_URL')
    
    temperature, humidity = get_hygrothermo()
    co2 = get_co2()
    
    response = requests.post(
        url,
        make_json(temperature, humidity, co2),
        headers={'Content-Type': 'application/json'}
    )
    
if __name__=='__main__':
    post_data()

cronの設定

定期実行するために crontab を設定します。

$ crontab -e

10分おきにスクリプトを実行するよう設定します(毎時0分、10分…50分)

ECOMONITOR_URL=https://script.google.com/macros/s/XXXXXXXXXXXXX/exec0,10,20,30,40,50 * * * * sudo -E python "ecomonitor.pyの格納されているパス" > /var/log/ecomonitor.log 2>&1

ソフトウェアの開発(Google Apps Script)

Typetalk ボットの作成

Typetalk ボットを作成します。ボットはTypetalkのトピック設定画面で簡単に作ることができます。Typetalk Token と、メッセージの取得と投稿のURLを後で使いますのでメモしておきます。

bot_settingボット設定画面

サーバー側のパラメータ設定

サーバー側の設定は、Google スプレッドシートの Setting シートで行えるようにします。
以下のようにパラメータを決めます。

パラメータ 設定値
topic_url メッセージを送るTypetalkトピックのURLを指定します。
token Typetalk Token を指定します。
mention 特定のユーザーにメッセージを送りたい場合、そのユーザーのユニークIDを指定します。
send_message Yesの場合、CO2濃度が閾値を超えた場合にメッセージを送ります。Yes以外の場合は送りません。
co2_high この値を超えるとCO2濃度を警告します。
co2_low この値を下回ったときに、お知らせします。

スプレッドシートにはこのように書きます。

parameterパラメータ設定画面

スクリプトの作成

受け取ったセンサーデータを、Google SpreadSheet に記録し Typetalkへポストするスクリプトを書きます。

function doPost(e) {
  var p = JSON.parse(e.postData.contents);

  ecomonitor(p);
}

function ecomonitor(p) {

  let conf  = new Config();
  let ss    = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName('monitor');

  sheet.appendRow([p.date, p.temperature, p.humidity, p.co2]);

  notifyTypetalk(conf, p)
}

function notifyTypetalk(conf, p) {

  let topicURL = conf.getParam('topic_url');
  let token = conf.getParam('token');
  let mention = conf.getParam('mention');
  let sendMessage = conf.getParam('send_message');
  let co2_high = conf.getParam('co2_high');
  let co2_low = conf.getParam('co2_low');
  let last_co2 = conf.getParam('last_co2');

  let message = null;

  if (sendMessage !== 'Yes') {
    return;
  }

  if (p.co2 >= co2_high && last_co2 < co2_high) {
    // CO2濃度が上限を超え、前回のCO2濃度が上限を超えていなかった場合にメッセージを通知する
    message = Utilities.formatString(
      "こんにちは!衛生委員です。\n" + 
      "なんと!お部屋のCO2濃度が %s ppm になっちゃってます。ちょーまずい!激ヤバです!\n" + 
      "適正なCO2濃度の目安は1000ppm以下みたいなので、こまめに換気しましょうね!\n" +
      "現在の気温は %s ℃、湿度は %s % です。",
      p.co2,
      p.temperature,
      p.humidity
    );
  }

  if (p.co2 <= co2_low && last_co2 > co2_low) {
    // CO2濃度が下限を下回り、前回のCO2濃度が下限を超えていた場合にメッセージを通知する
    message = Utilities.formatString(
      "こんにちは!衛生委員です。\nわーい、お部屋のCO2濃度が %s ppm に戻ったみたいですよ。よかったですね!\n" +
      "適正なCO2濃度の目安は1000ppm以下だそうです。これからもこまめな換気を心がけてくださいね!\n" +
      "現在の気温は %s ℃、湿度は %s % です。",
      p.co2,
      p.temperature,
      p.humidity
    );
  }

  conf.setParam('last_co2', p.co2);

  if (message) {
    let data = {
      'message' : mention + ' ' + message
    };
    let options = {
      'method'     : 'post',
      'contentType': 'application/x-www-form-urlencoded',
      'payload'    : data
    };
    let url = topicURL + '?typetalkToken=' + token;
    const result = UrlFetchApp.fetch(url, options);
    if(result.getResponseCode() !== 200) {
      throw new Error(result.getContentText());
    }
  }
}

// スプレッドシートから設定情報を読み書きする。
class Config {
  constructor() {
    let ss = SpreadsheetApp.getActiveSpreadsheet();
    this.sheet = ss.getSheetByName('Settings');
    this.colno_key   = 1;
    this.colno_value = 2;
    this.rowno = 2
  }

  getParam(key) {
    let value = ''
    for (let i = this.rowno; i <= this.sheet.getLastRow(); i++) {
      if (key == this.sheet.getRange(i, this.colno_key).getValue()) {
        value = this.sheet.getRange(i, this.colno_value).getValue();
        break;
      }
    }
    return value;
  }

  setParam(key, value) {
    for (let i = this.rowno; i <= this.sheet.getLastRow(); i++) {
      if (key == this.sheet.getRange(i, this.colno_key).getValue()) {
        value = this.sheet.getRange(i, this.colno_value).setValue(value);
        break;
      }
    }
  }
}

Google スプレッドシートに記録

受け取った値をスプレッドシートに記録します。

カラム名 説明
date 値を取得した日時
temperature 気温(℃)
humidity 湿度(%)
co2 CO2濃度(ppm)

このように記録されていきます。

spreadsheetスプレッドシートにデータを記録

ダッシュボードの作成

Googleデータポータルにログインしてダッシュボードを作成します。Googleデータポータルはクラウドベースの無料で使用できるBIツールで、様々なデータソースからデータをビジュアライズして表示することができます。

https://datastudio.google.com/

Google DataPortalGoogleデータポータル

今回はデータを記録したGoogleスプレッドシートをデータソースとして選択します。グラフや表を選び、どのような軸で表示するか設定していきます。

report_settingレポート設定画面

完成したダッシュボードです。
上段に現在の気温、湿度、CO2濃度を表示し、下に値の推移をグラフ表示します。表示する期間は右上のドロップダウンリストで切り替えることができます。いまは湿度とCO2濃度は適正値ですが、換気中のせいか少し気温が高めになっていますね。

dashboardダッシュボード

とある日のCO2濃度の推移です。この日はこまめに換気を心がけたせいか、適正値と言われる400ppm〜1000ppmの間をキープできたようです。仕事が終わって換気をやめたとたん、急激に濃度が上がっているのがわかります。

co2_graphCO2濃度の推移

Typetalkに通知

警告値を超えたらTypetalkに通知します。現在の気温と湿度も合わせて通知するようにしています。換気を終了するタイミングを知るため、CO2濃度が下がってきたときも通知します。

Typetalk

おわりに

いかがでしょうか?
Typetalkではとても簡単にボットを作ることができます。今回は IoTデバイスと連携させたシステムを作ってみました。ちょっとした身の回りの環境も数値化して記録してみることで、色々な気づきがあります。電子工作もブレッドボードを使えばはんだづけは不要ですし、データの蓄積やビジュアル化も無料で便利なサービスが色々出ていますので、興味を持たれた方は試してみてはいかがでしょうか。

無料で使えるフリープランでもボットは作れますので、とりあえず試してみたい方はこちらから!
https://www.typetalk.com/

Typetalkのボットの作り方をさらに詳しく知りたい方は、こちらをご覧ください。
https://www.typetalk.com/ja/blog/post-a-message-to-typetalk-using-a-bot/

それでは、安全で快適なテレワークを楽しみましょう!

開発メンバー募集中

より良いチームワークを生み出す

チームの創造力を高めるコラボレーションツール

製品をみる