RaspberryPi3で温湿度を取得してSlackに通知する

##0.はじめに
ラズパイで何か役に立つものを作りたいと思い、これをやってみました。
Slackで毎日AM１１時に、部屋の温湿度の通知が来ることを目標にして作っていきます。

尚、基本的には下記の記事を参考にしています。ありがとうございました。
[【30分IoT】Raspberry PiとSlackを連携させて温湿度センサーの値(AM2302)を投稿してみよう](http://www.mana-cat.com/entry/2017/05/06/115111)

##1.必要なもの
以下を使って作成しました。

- RaspberryPi3
- 温湿度センサモジュール（DHT11）
- ブレッドボード
- 抵抗10kΩ 1本
- ジャンパーケーブル(オスXメス) 3本

##2.環境
次のことが準備できている前提で進みます。

- ラズパイのOSインストール、初期設定が完了している。
- 必要なものが揃っている。
- Slackのサインアップが終わっている。

##3.配線する
ラズパイの電源を落として下図のように配線をします。
今回GPIOの接続は4番ピンにしています。
この後、温度を取得する時に指定するので、4番でない場合は何番にしたか覚えておきましょう。

DHT11
![スクリーンショット 2018-03-11 15.20.14.png](https://qiita-image-store.s3.amazonaws.com/0/88234/b5326df4-2240-440d-1839-8c0a0eb2e7fa.png)

![スクリーンショット 2018-03-11 12.17.35.png](https://qiita-image-store.s3.amazonaws.com/0/88234/ccfece89-4c43-9309-20df-063a0c403d42.png)

##4.Slackの準備をする
次にSlackに受信するためのAPIの導入をしていきます。
以下のページから設定を行います。
[https://my.slack.com/services/new/incoming-webhook/](https://my.slack.com/services/new/incoming-webhook/)

-1.チャンネルを選択して、「着信Webフックインテグレーションの追加」をクリックします。
![スクリーンショット 2018-03-11 12.43.30.png](https://qiita-image-store.s3.amazonaws.com/0/88234/02e6c68a-2d61-86ab-3438-1485ffc16424.png)

-2.このURLはPythonのプログラムで使用します。
![スクリーンショット 2018-03-11 12.46.16.png](https://qiita-image-store.s3.amazonaws.com/0/88234/89e1819b-e1eb-c1fb-2bc4-a5026e9baacf.png)

-3.設定を保存する。名前やアイコンは自由に変更することができます。
![スクリーンショット 2018-03-11 14.14.38.png](https://qiita-image-store.s3.amazonaws.com/0/88234/75c22356-020b-2889-dfc4-caa84511e114.png)

##5.温湿度を取得する
次にDHT11を使って、温度や湿度を取得するプログラムを用意するのですが、
これはすでに先人の方が用意をしてくれております。（本当にありがとうございます。）

そこで今回はそのプログラムをgitから取得する手順を載せます。

-1.Slackwebをインストールします。
まずはラズパイからSlackに通知するための準備として、Slackwebをインストールします。
ラズパイからターミナルを開いてください。

```terminal:terminal
$ sudo pip install slackweb
Collecting slackweb
  Downloading slackweb-1.0.5.tar.gz
Building wheels for collected packages: slackweb
  Running setup.py bdist_wheel for slackweb ... done
  Stored in directory: /root/.cache/pip/wheels/3e/40/3e/787721594a76a65d717864ed83b21b5dfc56cc93598d872157
Successfully built slackweb
Installing collected packages: slackweb
Successfully installed slackweb-1.0.5
```
-2.温度を取得するプログラムをgitから取得します。
gitをインストールしていない方は、gitをインストールします。

```terminal:terminal
$ sudo apt-get install git
```
任意のディレクトリで下記を実行します。

```terminal:terminal
$ git clone https://github.com/szazo/DHT11_Python.git
Cloning into 'DHT11_Python'...
remote: Counting objects: 48, done.
remote: Total 48 (delta 0), reused 0 (delta 0), pack-reused 48
Unpacking objects: 100% (48/48), done.
```
このような構成になっていると思います。

```terminal:terminal
/DHT11_Python $ ls -l
合計 24
-rw-r--r-- 1 pi pi 1071  2月  4 13:39 LICENSE.md
-rw-r--r-- 1 pi pi  899  2月  4 13:39 README.md
-rw-r--r-- 1 pi pi    1  2月  4 13:39 __init__.py
-rw-r--r-- 1 pi pi 5969  2月  4 13:39 dht11.py
-rw-r--r-- 1 pi pi  464  2月  4 13:39 dht11_example.py
```

```python:dht11.py
import time
import RPi


class DHT11Result:
    'DHT11 sensor result returned by DHT11.read() method'

    ERR_NO_ERROR = 0
    ERR_MISSING_DATA = 1
    ERR_CRC = 2

    error_code = ERR_NO_ERROR
    temperature = -1
    humidity = -1

    def __init__(self, error_code, temperature, humidity):
        self.error_code = error_code
        self.temperature = temperature
        self.humidity = humidity

    def is_valid(self):
        return self.error_code == DHT11Result.ERR_NO_ERROR


class DHT11:
    'DHT11 sensor reader class for Raspberry'

    __pin = 0

    def __init__(self, pin):
        self.__pin = pin

    def read(self):
        RPi.GPIO.setup(self.__pin, RPi.GPIO.OUT)

        # send initial high
        self.__send_and_sleep(RPi.GPIO.HIGH, 0.05)

        # pull down to low
        self.__send_and_sleep(RPi.GPIO.LOW, 0.02)

        # change to input using pull up
        RPi.GPIO.setup(self.__pin, RPi.GPIO.IN, RPi.GPIO.PUD_UP)

        # collect data into an array
        data = self.__collect_input()

        # parse lengths of all data pull up periods
        pull_up_lengths = self.__parse_data_pull_up_lengths(data)

        # if bit count mismatch, return error (4 byte data + 1 byte checksum)
        if len(pull_up_lengths) != 40:
            return DHT11Result(DHT11Result.ERR_MISSING_DATA, 0, 0)

        # calculate bits from lengths of the pull up periods
        bits = self.__calculate_bits(pull_up_lengths)

        # we have the bits, calculate bytes
        the_bytes = self.__bits_to_bytes(bits)

        # calculate checksum and check
        checksum = self.__calculate_checksum(the_bytes)
        if the_bytes[4] != checksum:
            return DHT11Result(DHT11Result.ERR_CRC, 0, 0)

        # ok, we have valid data, return it
        return DHT11Result(DHT11Result.ERR_NO_ERROR, the_bytes[2], the_bytes[0])

    def __send_and_sleep(self, output, sleep):
        RPi.GPIO.output(self.__pin, output)
        time.sleep(sleep)

    def __collect_input(self):
        # collect the data while unchanged found
        unchanged_count = 0

        # this is used to determine where is the end of the data
        max_unchanged_count = 100

        last = -1
        data = []
        while True:
            current = RPi.GPIO.input(self.__pin)
            data.append(current)
            if last != current:
                unchanged_count = 0
                last = current
            else:
                unchanged_count += 1
                if unchanged_count > max_unchanged_count:
                    break

        return data

    def __parse_data_pull_up_lengths(self, data):
        STATE_INIT_PULL_DOWN = 1
        STATE_INIT_PULL_UP = 2
        STATE_DATA_FIRST_PULL_DOWN = 3
        STATE_DATA_PULL_UP = 4
        STATE_DATA_PULL_DOWN = 5

        state = STATE_INIT_PULL_DOWN

        lengths = [] # will contain the lengths of data pull up periods
        current_length = 0 # will contain the length of the previous period

        for i in range(len(data)):

            current = data[i]
            current_length += 1

            if state == STATE_INIT_PULL_DOWN:
                if current == RPi.GPIO.LOW:
                    # ok, we got the initial pull down
                    state = STATE_INIT_PULL_UP
                    continue
                else:
                    continue
            if state == STATE_INIT_PULL_UP:
                if current == RPi.GPIO.HIGH:
                    # ok, we got the initial pull up
                    state = STATE_DATA_FIRST_PULL_DOWN
                    continue
                else:
                    continue
            if state == STATE_DATA_FIRST_PULL_DOWN:
                if current == RPi.GPIO.LOW:
                    # we have the initial pull down, the next will be the data pull up
                    state = STATE_DATA_PULL_UP
                    continue
                else:
                    continue
            if state == STATE_DATA_PULL_UP:
                if current == RPi.GPIO.HIGH:
                    # data pulled up, the length of this pull up will determine whether it is 0 or 1
                    current_length = 0
                    state = STATE_DATA_PULL_DOWN
                    continue
                else:
                    continue
            if state == STATE_DATA_PULL_DOWN:
                if current == RPi.GPIO.LOW:
                    # pulled down, we store the length of the previous pull up period
                    lengths.append(current_length)
                    state = STATE_DATA_PULL_UP
                    continue
                else:
                    continue

        return lengths

    def __calculate_bits(self, pull_up_lengths):
        # find shortest and longest period
        shortest_pull_up = 1000
        longest_pull_up = 0

        for i in range(0, len(pull_up_lengths)):
            length = pull_up_lengths[i]
            if length < shortest_pull_up:
                shortest_pull_up = length
            if length > longest_pull_up:
                longest_pull_up = length

        # use the halfway to determine whether the period it is long or short
        halfway = shortest_pull_up + (longest_pull_up - shortest_pull_up) / 2
        bits = []

        for i in range(0, len(pull_up_lengths)):
            bit = False
            if pull_up_lengths[i] > halfway:
                bit = True
            bits.append(bit)

        return bits

    def __bits_to_bytes(self, bits):
        the_bytes = []
        byte = 0

        for i in range(0, len(bits)):
            byte = byte << 1
            if (bits[i]):
                byte = byte | 1
            else:
                byte = byte | 0
            if ((i + 1) % 8 == 0):
                the_bytes.append(byte)
                byte = 0

        return the_bytes

    def __calculate_checksum(self, the_bytes):
        return the_bytes[0] + the_bytes[1] + the_bytes[2] + the_bytes[3] & 255
```

```python:dht11_example.py
import RPi.GPIO as GPIO
import dht11
import time
import datetime

# initialize GPIO
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.cleanup()

# read data using pin 14
instance = dht11.DHT11(pin=14)

while True:
    result = instance.read()
    if result.is_valid():
        print("Last valid input: " + str(datetime.datetime.now()))
        print("Temperature: %d C" % result.temperature)
        print("Humidity: %d %%" % result.humidity)

    time.sleep(1)
```
-3.取得したpythonプログラムを修正します。
dht11_example.pyを以下のように修正します。

```python:dht11_example.py
# coding:utf-8
import RPi.GPIO as GPIO
import dht11
import time
import datetime
import slackweb #←追加

# initialize GPIO
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.cleanup()

# read data using pin 4
instance = dht11.DHT11(pin=4) #←配線したGPIOピンの番号を指定

#取得した情報をslackwebに通知する
result = instance.read()
temp = result.temperature
humidity = result.humidity
nowtime = "{0:%Y/%m/%d %H:%M:%S}".format(datetime.datetime.now())
if humidity is not None and temp is not None:
    msg = u"{0}現在の温度は{1:0.1f}度、湿度は{2:0.1f}% です".format(nowtime,temp, humidity)
else:
    msg = u"温湿度を測定できませんでした"

slack = slackweb.Slack(url="https://hooks.slack.com/services/...")#手順4-2のURLを記載する
slack.notify(text=msg)
print msg
```

ファイル名を変更します。(任意)

```terminal:terminal
$ mv dht11_example.py notify_temperature.py
```

実行してこのように出力されればOKです。

```terminal:terminal
$ python ./notify_temperature.py
2018/02/04 14:27:57現在の温度は18.0度、湿度は19.0% です 
```

##6.cronを設定する
ラズパイで定期的に起動して、温湿度を取得する場合はcronに登録します。

-1.cronを起動します。

```terminal:terminal
$ sudo /etc/init.d/cron start
[ ok ] Starting cron (via systemctl): cron.service.
$ sudo apt-get install chkconfig
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています                
状態情報を読み取っています... 完了
以下の追加パッケージがインストールされます:
  insserv
提案パッケージ:
  bootchart2
以下のパッケージが新たにインストールされます:
  chkconfig insserv
アップグレード: 0 個、新規インストール: 2 個、削除: 0 個、保留: 94 個。
67.5 kB のアーカイブを取得する必要があります。
この操作後に追加で 196 kB のディスク容量が消費されます。
続行しますか? [Y/n] Y
取得:1 http://ftp.tsukuba.wide.ad.jp/Linux/raspbian/raspbian stretch/main armhf chkconfig all 11.4.54.60.1debian1 [9,766 B]
取得:2 http://ftp.tsukuba.wide.ad.jp/Linux/raspbian/raspbian stretch/main armhf insserv armhf 1.14.0-5.4 [57.7 kB]
67.5 kB を 11秒 で取得しました (6,014 B/s)
以前に未選択のパッケージ chkconfig を選択しています。
(データベースを読み込んでいます ... 現在 125206 個のファイルとディレクトリがインストールされています。)
.../chkconfig_11.4.54.60.1debian1_all.deb を展開する準備をしています ...
chkconfig (11.4.54.60.1debian1) を展開しています...
以前に未選択のパッケージ insserv を選択しています。
.../insserv_1.14.0-5.4_armhf.deb を展開する準備をしています ...
insserv (1.14.0-5.4) を展開しています...
chkconfig (11.4.54.60.1debian1) を設定しています ...
man-db (2.7.6.1-2) のトリガを処理しています ...
insserv (1.14.0-5.4) を設定しています ...
$ sudo chkconfig cron
cron  on
```

-2.cronに登録する。
cronを編集し、以下を追記する。

```terminal:terminal
$ crontab -e
```
毎日AM１１時に起動する場合
00 11 * * * python [ディレクトリパス]/notify_temperature.py

実行するとSlackで通知がきます。
![スクリーンショット 2018-03-11 15.30.35.png](https://qiita-image-store.s3.amazonaws.com/0/88234/83c4dd07-3d0a-cf9e-fabe-9cc6a346223a.png)


##7.おわりに
無事完成できたでしょうか。私は配線のところで間違えてしまい、苦戦しました。

以上、ありがとうございました。