ラズパイで廉価だけどちょっと立派な温度センサDHT21(AM2302)をDHT11の気軽さで使いたい

#猛暑到来
５月では観測事情まれにみる猛暑。我が家もあっついです。家では猫が留守番しているのですが、高齢というのもあってちと心配。部屋が高温になったらお知らせするボットを作れないかなあと、とりあえずセンサの巻。

#調達する
Amazonか秋月のような電子部品屋に売ってます。Amazonすごいな。

##DHT11が定番だけど
とりあえずラズパイで温度といえばDHT11。安いしGPIOで繋げられるし、ネットにライブラリが転がってる。ですけど、、、測定誤差±２℃っていくらなんでも大きすぎじゃないですか？そんなに精度はいらないけど、もう一息ほしいところです。

##ということでDHT22を購入
DHT22を購入。DHT11に比べると高いけど、まあ安い（８００円位）。
買うときはセンサ単体と基盤に実装済のものがあります。
板に実装する予定がないときは実装済みのほうが楽だと思います。ジャンパピンまでついてるし。
![スクリーンショット 2019-05-29 10.08.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388857/5e776a5d-b550-3343-3507-d77c17eea4a4.png)
###基盤実装品で買う場合
同じ外見をしていても制作業者によって質が様々のようです。レビューを見て慎重に買ってください。
僕はHiletgo制を買いました。
あとピンのアサインが業者やロットによって様々なので、配線は基盤の表記を見て行ってください。僕は何も考えずにネットのとおりに配線したらDHT11が焼け落ちました。
###センサ単体で買う場合
こんな感じに抵抗を入れる必要があるそうです。詳しくはデータシートを。
![スクリーンショット 2019-05-29 10.18.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388857/c69410b3-8cae-be74-1f56-f31b13869be3.png)

#とりあえず使ってみる。
とりあえずDHT11のライブラリそのままで使えないかな、ということでやってみます。

##配線
僕の買ったものはDAT/VCC/GNDの３つがあったので以下のように配線
DAT　->　７（GPIO4）　｜　VCC ->　１　｜ GND　->　９

VCCはデータシートを見ると6.0Vまで動くようなので、3.3Vのピンが余ってなければ５Vピンでも行けると思います。

##DHT11用ライブラリの導入
gitに転がっているDHT11ライブラリをcloneします。
gitをインストールしていない人はインストール

```
sudo apt-get install git
```

そんで

```
git clone https://github.com/szazo/DHT11_Python.git
```

そうするとカレントディレクトリに必要なファイルがダウンロードされます。

```
tree
 ├── DHT11_Python
    ├── dht11_example.py
    ├── dht11.py
    ├── __init__.py
    ├── LICENSE.md
    └── README.md
```

参考：Raspberry PiとDHT11で温度・湿度を測る
https://qiita.com/mininobu/items/1ba0223af84be153b850

##使ってみる
ピン番号だけ書き換え。

```［変更点］dht11_example.py
instance = dht11.DHT11(pin=4)
```

そんで実行


```
python dht11_example.py 

Last valid input: 2019-05-29 03:23:42.870835
Temperature: 1 C
Humidity: 2 %
Last valid input: 2019-05-29 03:23:46.116768
Temperature: 1 C
Humidity: 2 %
```

だめです！
我が家はそんなに寒くない。
#dht11.pyを改造する
DHT22ってなんか情報が少ないんですよね、調べ不足かもしれませんが。dht11のライブラリ書き換えてなんとかなんないかなあ。
##のぞいてみる
センサーからの読み出しの核になってるのはdht11.py、これをのぞいてみるとCRCまでチェックしてくれててエラー出てない。読み出しのフォーマットとかバイト数はDHT11と一緒みたい。
ソースのぞくとDHT11/22が送ってくるシリアルデータは５バイト。DHT11のビット配置は下のようになっています。DHT11は測定範囲が狭いから温度データは１バイトで十分なんですね。

![スクリーンショット 2019-05-29 11.35.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388857/b5500df2-c38d-99e8-9e7c-2c2d3daeab98.png)
順当に考えると、精度の高いDHT22は湿度/温度２バイトづつなんですけど、２バイトの小数扱う変数ってありましたっけ？

###ローデータを出してみる。
```
root@raspberrypi:/home/pi# python dht22_dump.py 
[1, 241, 1, 10, 253]
```
これを単純に2バイト整数として見ると
　　　湿度：４９７　　｜　　温度：２６６      

小数点型を使ってるわけではなくて、2バイト整数型で読んで、１０で割るといいみたいです。

##dht22.pyを作る
dht11.pyからの変更点はこちら。
もっとスマートな方法があるのかな、

```[変更点]dht22.py
        #追加
        temp = (float(the_bytes[2]) * 256 + the_bytes[3]) / 10
        hum  = (float(the_bytes[0]) * 256 + the_bytes[1]) / 10

        #変更
        return DHT22Result(DHT22Result.ERR_NO_ERROR, temp, hum)
```
あとclass名をDHT22にした。


```dht22.py
# -*- coding: utf-8 -*-

import time
import RPi


class DHT22Result:
    'DHT22 sensor result returned by DHT22.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 == DHT22Result.ERR_NO_ERROR


class DHT22:
    'DHT22 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 DHT22Result(DHT22Result.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 DHT22Result(DHT22Result.ERR_CRC, 0, 0)
            
        #dump時に使用
        #print(str(the_bytes) )
        #追加
        temp = (float(the_bytes[2]) * 256 + the_bytes[3]) / 10
        hum  = (float(the_bytes[0]) * 256 + the_bytes[1]) / 10
        
        #変更
        return DHT22Result(DHT22Result.ERR_NO_ERROR, temp, hum)

    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
```

クラス名をDHT22に変更したのでサンプルも少し変更。
あとGPIOは結構入れ替えるので、ピン番号を最初に出しました。


```dht22_example.py
import RPi.GPIO as GPIO
import dht22
import time
import datetime

GPIO_PIN = 4

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

# read data using pin GPIO_PIN
instance = dht22.DHT22(pin=GPIO_PIN)

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

    time.sleep(1)
```

##動かしてみるよ

```
python dht22_example.py 
Last valid input: 2019-05-29 04:27:36.415902
Temperature: 28.5 C
Humidity: 53.7 %
```

温度は結構正確だけど、湿度は手元の温度湿度計と比べると4%くらい高め。
他のひとの結果を見てもそんな感じだからここは補正が必要かな。

#これから
とりあえず自宅の温度は確認できるようになりました。ただこののままじゃ不便なので、敷居の温度を超えたらSlackに通知して、冷房のONを促す仕組みを作ろうと思います。

夏がくる前に！