RaspberryPiを使って洗濯乾燥タイマーを作る

# RaspberryPiを使って洗濯乾燥タイマーを作る

## はじめに（制作の目的）
- 会社勤めのため、日中、不在にしていることが多く、にわか雨が降ったときに洗濯物の取り込みができません。このため洗濯物については、基本的に室内干しで乾燥させています。
- ところが、物干し部屋の日当たりが悪く、冬場だとなかなか思うように乾燥が進まないため、取り込むタイミングを逸することがあり、洗濯物が雪ダルマ式に溜まるという悪循環に陥っています。
- この悪循環を断ち切る（？）ため、洗濯物の乾燥完了時間を計算し、スマートフォンに通知する仕組みを作りました。

## 用意するもの
①デバイス側の環境構築に必要なもの。秋月電子で購入可能です。
http://akizukidenshi.com/catalog/top.aspx

②サーバ側の環境構築に必要なもの。AWSとSlackのアカウントが必要です。
AWS: https://aws.amazon.com/jp/
Slack: https://slack.com/intl/ja-jp/

|品名|数量|用途|
|:--|:--|:--|
|① RaspberryPi ZeroWH|1|制御用マイコン|
|① ブレッドボード|1|回路構築用|
|① DHT11|1|温湿度センサー|
|① 抵抗（4.7kΩ）|2||
|① 抵抗（1kΩ）|1||
|① タクトスイッチ|1|起動・停止スイッチ|
|① LED|1|起動時に点灯（目視確認用）|
|② AWS IoT Core||端末との通信を終端。MQTTブローカーの役割。|
|② AWS Lambda||IoTCoreのバックエンド。Slackとの通信を仲介。|
|② Slack||スマートフォンへの通知に利用|

## システム概要
RaspberryPi --- AWS IoT Core --- AWS Lambda --- Slack --- スマホ

- RaspberryPi ~ AWS IoT Core
    - MQTT over TLS。[AWS IoT Device SDK for Python](https://docs.aws.amazon.com/ja_jp/greengrass/latest/developerguide/IoT-SDK.html)を使って接続します。
- AWS IoT Core ~ AWS Lambda
    - AWS IoT Coreのアクション定義でMQTT topic受信時にLambdaをトリガーします。
- AWS Lambda ~ Slack
    - Lambda関数でSlackの[Incoming Webhook](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)のAPIをキックします。
- Slack ~ スマホ
    - スマホにSlackアプリをインストールして、Slackからの通知を受信します。
   
## 環境構築（サーバ）
### AWS IoT Core

##### 証明書の作成
AWS IoT Coreとの接続認証に必要なX.509証明書を作成します。作成手順として、まずポリシー（AWSリソースへの認可情報）を事前に作成してから、証明書に対してポリシーをアタッチしていきます。

- AWSマネジメントコンソール上でAWS IoT Coreサービスを選択し、「安全性」→「ポリシー」からポリシーの作成を行います。ポリシー名は任意の値を設定します。アクションは「iot:＊」、リソースARNは「＊」として、任意の名前でポリシーを作成します。

- 「安全性」→「証明書」から証明書の作成を行います。1-Click 証明書作成メニューから証明書を作成し、「このモノの証明書」「プライベートキー」をPCにダウンロードします。この証明書とプライベートキーはAWS IoT SDKの設定で後ほど必要となります。また同じく必要なルートCA証明書については、[こちら](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/server-authentication.html#server-authentication-certs)からRSA 2048 ビットキーの内容をテキストエディタに保存して作成します。
- 「有効化」を押して証明書を有効にしてから、「ポリシーをアタッチ」で先ほど作成したポリシーを証明書にアタッチします。

##### デバイス（モノ）の登録
AWS IoT Coreに接続するデバイスの登録を行います。

- AWSマネジメントコンソールからAWS IoT Coreサービスを選択し、「管理」→「モノ」→「作成」→「単一のモノを作成する」より、画面の指示に従って、デバイス（モノ）の登録を行います。証明書の新規作成を促されますが、先ほど作成した証明書を使うため、「証明書なしでモノを作成」を選びます。
- 「安全性」→「証明書」で先ほど作成した証明書のカード右上のサブメニューにある「モノをアタッチ」をクリックして、先ほど作成したデバイス（モノ）を証明書にアタッチします。

これでAWS IoT Coreへの接続に必要な設定は完了です。
##### ルールの作成
次にAWS IoT Coreで受信したデータをAWS Lambdaに転送するための設定を行います。

- AWSマネジメントコンソールからAWS IoT Coreサービスを選択し、「ACT」→「ルール」→「ルールの作成」より、画面の指示に従って、転送ルールを作成します。
- ルールクエリステートメントでは受信したMQTTメッセージに対して、アクションを適用する条件をSQLで指定します。今回のケースではtopic名="condition"、且つnotice=1の条件でLambda関数を呼び出すため、（SELECT * FROM 'condition' WHERE notice = 1）としています。
- Lambda関数を呼び出すための設定は、「1 つ以上のアクションを設定する」→「アクションの追加」から行います。

### AWS Lambda
AWSマネジメントコンソールから、Lambdaサービスを選択し、AWS IoT Coreから呼び出されるLambda関数の定義を行います。Slackへの通知はIncoming Webhook URLに対して、"payload=" + json.dumps(send_data)の形式でテキストデータを作成しPOSTします。

```python
import json
import urllib.request

def lambda_handler(event, context):

    d = json.dumps(event)
    post_slack(event)
    
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

def post_slack(event):
    
    t = event["remaining_time"]
    
    # メッセージ作成
    if t < 0:
        message = "洗濯物が乾きました！"
    else:
        message = "あと%-2i分で洗濯物が乾きます。" % t
    
    send_data = {
        "username" : "dry_notice",
        "text" : message,
    }
    
    send_text = "payload=" + json.dumps(send_data)
    
    # slackへの通知
    request = urllib.request.Request(
        "SlackのIncomming WebhookのURL", 
        data=send_text.encode('utf-8'), 
        method="POST"
    )
    with urllib.request.urlopen(request) as response:
        response_body = response.read().decode('utf-8')
    
```
### Slack
Slackでアカウントを作成し、任意のチャンネルを追加します。その後、追加したチャンネルに対してIncoming Webhookの登録を行います。

これでサーバ側の環境構築は完了です。

## 環境構築（デバイス）
### ハードウェア設計
- ブレッドボードを使って、RaspberryPiと①の各パーツを配線します。RaspberryPiのピン配置は[こちら](https://tool-lab.com/raspberrypi-startup-22/)がわかりやすく纏まっていて参考になるかと思います。
- 水色の四角い箱が[DHT11](http://akizukidenshi.com/catalog/g/gM-07003/)になります。左から5V電源、DATA、NC（未使用）、GNDになります。DATAは任意のGPIO（下図では14Pin）に接続します。またDATAラインには4.7kΩの抵抗を接続し、5vでプルアップしておきます。
- そのほか、LEDとタクトスイッチは1kΩ、4.7kΩの抵抗で任意のGPIO（下図では23,24Pin）に接続します。

<img width="536" alt="kairozu.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/576521/4fc32144-6d71-52e8-2585-b7178ef37025.png">

### ソフトウェア設計
全体の処理の流れとしては以下のようになります。

- GPIO24のPull_UPイベントをトリガーとして、モニターを起動する。
- dht11から10秒毎に温度・湿度値を取得する。
- 洗濯物の重量と温度・湿度値から乾燥するまでの残り時間を計算する。
- （残り時間が10分以下になったら）AWS IoT Core経由で残り時間10分の通知を行う。
- （残り時間が0になったら）AWS IoT Core経由で乾燥完了の通知を行う。
- 乾燥完了もしくはGPIO24のPull_UPイベントによりモニターを停止する。

```python
#!/usr/bin/python3
# coding: UTF-8

# Import SDK packages
import RPi.GPIO as GPIO
import dht11　#・・・1
import time
import datetime
from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTShadowClient,AWSIoTMQTTClient
from AWSIoTPythonSDK.exception import AWSIoTExceptions
import json
import logging

# Init logging
logging.basicConfig(filename="ログファイルのパス",level=logging.INFO,format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# Init AWSIoTMQTTClient　#・・・2
myAWSIoTMQTTClient = None
myAWSIoTMQTTClient = AWSIoTMQTTClient("AWS IoT Coreのモノの名称")
myAWSIoTMQTTClient.configureEndpoint("AWS IoT CoreのエンドポイントURL", 8883)
myAWSIoTMQTTClient.configureCredentials("ルート証明書のパス", "秘密鍵のパス", "クライアント証明書のパス")

# AWSIoTMQTTClient connection configuration
myAWSIoTMQTTClient.configureAutoReconnectBackoffTime(1, 32, 20)
myAWSIoTMQTTClient.configureOfflinePublishQueueing(-1)  # Infinite offline Publish queueing
myAWSIoTMQTTClient.configureDrainingFrequency(2)  # Draining: 2 Hz
myAWSIoTMQTTClient.configureConnectDisconnectTimeout(10)  # 10 sec
myAWSIoTMQTTClient.configureMQTTOperationTimeout(5)  # 5 sec

# Connect to AWS IoT
myAWSIoTMQTTClient.connect()
logging.info('connect to AWS IoT')

# MQTT topic　#・・・3
topic_1 = "condition"
topic_2 = "monitermode"

# Variable
modeState = False　# Operating state(True:ON False:OFF)
v = 3000　# Laundry weight
delta_v = 0 # Delta of Laundry weight
notice = 0　# Notification to Slack
flg_1 = True
flg_2 = True
starttime = datetime.datetime.now()
lasttime = starttime

# Init GPIO
LED = 23
SWITCH = 24
GPIO.setwarnings(True)
GPIO.setmode(GPIO.BCM)
GPIO.setup(LED,GPIO.OUT)
GPIO.setup(SWITCH,GPIO.IN)

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

# Action when switch is pressed
def switch_on(self):

    # When it is running
    if modeState:
        GPIO.output(LED,0)
        modeState = False
        logging.info("Stop drying monitor")
        payload = { "mode": 0}

    # When stopped
    else:
        GPIO.output(LED,1)
        modeState = True
        v = 3000
        delta_v = 0
        notice = 0
        flg_1 = True
        flg_2 = True
        starttime = datetime.datetime.now()
        lasttime = starttime
        logging.info("Start drying monitor")
        payload = { "mode": 1}

    myAWSIoTMQTTClient.publish(topic_2, json.dumps(payload), 1)

# GPIO event setting　#・・・4
GPIO.add_event_detect(SWITCH,GPIO.RISING,callback=switch_on,bouncetime=200)


while True:
    try:
        # When it is running
        if modeState:
            result = instance.read()
            if result.is_valid():
                logging.info("Temperature: %-3.1f C" % result.temperature)
                logging.info("Humidity: %-3.1f %%" % result.humidity)

                now = datetime.datetime.now()
                delta = now - lasttime
                logging.info("delta_time: %-2i sec" % delta.seconds)
                lasttime = now

                #・・・5
                ps = 6.11 * 10 ** (7.5 * result.temperature / (result.temperature + 237.3))
                delta_v += (-0.45 * ps * (1-result.humidity / 100) + 0.25) * delta.seconds / 60
                logging.info("delta_v: %-3i g" % int(delta_v))

                elapsed_time = lasttime - starttime
                estimate_time = elapsed_time.seconds * v / (-1 * delta_v) / 60
                remaining_time = elapsed_time.seconds * (v / (-1 * delta_v) - 1) / 60
                logging.info("estimate_time: %-3.1f minutes" % estimate_time)
                logging.info("remaining_time: %-3.1f minutes" % remaining_time)

                #・・・6
                if remaining_time < 10:
                    if flg_1:
                        notice = 1
                        flg_1 = False
                    else:
                        notice = 0

                if remaining_time < 0:
                    if flg_2:
                        notice = 1
                        flg_2 = False
                        modeState = False
                    else:
                        notice = 0

                payload = { "time":str(datetime.datetime.now()),\
                            "temperature":round(result.temperature,1),\
                            "humidity":round(result.humidity,1),\
                            "estimate_time":round(estimate_time,1),\
                            "remaining_time":round(remaining_time,1),\
                            "notice":notice}

                myAWSIoTMQTTClient.publish(topic_1, json.dumps(payload), 1)
        
        # When stopped
        else:
            GPIO.output(LED,0)

        time.sleep(6)

    except KeyboardInterrupt:
        break

    except:
        pass

logging.warning("Exception detected. Finish application.")
GPIO.cleanup()
myAWSIoTMQTTClient.disconnect()
```
1. dht11はセンサーから温度・湿度の値を取得するライブラリです。[GitHub](https://github.com/szazo/DHT11_Python)から入手できます。

2. AWS IoT Coreに接続するMQTTクライアントの設定。いずれもAWS Iot Coreのメニューで参照した内容を設定します。
    - モノの名前：「管理」→「モノ」に登録したデバイス名称
    - エンドポイント：「設定」→「カスタムエンドポイント」に表示されるURL
    - 各種証明書：「安全性」→「証明書」から「モノ」にアタッチした証明書をダウンロードして、RaspberryPiに格納しておきます。
3. MQTTのトピック名。topic_1（condition）は温度・湿度などのセンサー値、topic_2（monitermode）はモニター状態を投稿するためのトピック。
4. タクトスイッチの接続先であるGPIO24の監視設定。スイッチを押す（Lo→Hi）とswitch_on関数を起動します。switch_on関数ではLED制御とパラメータの初期化を行います。modeStateでモニター状態を保持しており、スイッチを押すたびに状態（OFF/ON）を遷移させています。
5. 乾燥時間を計算する処理部です。乾燥時間の数式は[こちらのサイト](https://www.frame-diy.com/2017/04/01/mathematical/)を参考にしています。vは洗濯カゴを洗濯物で一杯にした時の重量（乾燥前-乾燥後→水分重量）、delta_vはスイッチで起動開始してから乾燥した水分重量です。
6. 残り時間（remaining_time）が①10分以下、②0分以下となった時点でtopic_1への投稿を行います。topic_1には以下のデータを投稿します。

|項目名|内容|
|:--:|:--:|
|time|現在時刻|
|temperature|温度（℃）|
|humidity|湿度（％）|
|estimate_time|乾燥完了するまでの所要時間（分）|
|remaining_time|残り時間（分）|
|notice|Slackへの通知フラグ（1:通知する 0:通知しない）|

## おわりに
時間があれば、拡張機能として、扇風機を制御して乾燥時間を短縮する機能を追加したいと思っています。