IoTで出社／退社をワンタッチ化

こんにちは！[IoTLT Advent Calendar 2018](https://qiita.com/advent-calendar/2018/iotlt_neo) 12日目担当のchinoppyです！

真ん中の日に、日本の真ん中の長野から再び？！お送りします。

会社の出社／退社状態を管理しているWebページ（以下、ホワイトボードと記載）をIoTを使ってワンタッチで状態（出勤／退勤）を変更できるようにしてみました。
これで、1秒でも早く仕事を始めて、1秒でも早く帰宅できるようになりましたｗ

【ホワイトボード】
![image.png](https://qiita-image-store.s3.amazonaws.com/0/52947/c266b287-8a48-6eb0-5f40-8a8b660cd4f9.png)

## きっかけ

- もともとは、物理ホワイトボードだったものを、社員の有志（ @moonwalkerpoday ）が電子化
- これによりslackから状態を変更可能にしたりと簡素化する動きがでてきた
- そんななか、社内のガジェットをあさっていたら・・・

### 社内にあったガジェット等

#### [ソニー SONY 非接触ICカードリーダー/ライター PaSoRi](https://www.amazon.co.jp/%E3%82%BD%E3%83%8B%E3%83%BC-SONY-%E9%9D%9E%E6%8E%A5%E8%A7%A6IC%E3%82%AB%E3%83%BC%E3%83%89%E3%83%AA%E3%83%BC%E3%83%80%E3%83%BC-PaSoRi-RC-S380/dp/B00948CGAG/ref=sr_1_1?ie=UTF8&qid=1544353076&sr=8-1&keywords=%E3%83%91%E3%82%BD%E3%83%AA)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/52947/e46c38cb-caf1-ba2e-48b4-b4afbd226cfb.png)

#### Raspberry Pi 3 Model B + [Raspberry Pi Sense HAT](https://www.amazon.co.jp/Raspberry-Pi-Sense-HAT-LED%E3%83%9E%E3%83%88%E3%83%AA%E3%83%83%E3%82%AF%E3%82%B9%E3%80%81%E3%82%B8%E3%83%A7%E3%82%A4%E3%82%B9%E3%83%86%E3%82%A3%E3%83%83%E3%82%AF/dp/B014N3RAUC/ref=sr_1_1?ie=UTF8&qid=1544352775&sr=8-1&keywords=sensehat) 付き

![image.png](https://qiita-image-store.s3.amazonaws.com/0/52947/2755e029-d9f3-a572-1964-588c3447f178.png)

ちなみに、Sense Hatは[Astro Pi](https://astro-pi.org/)プロジェクトというのために開発されたものなのですね。
なんと、宇宙空間で使うことを想定したものだそうです！夢を感じる。

#### [TP-Link WiFiスマートプラグ](https://www.amazon.co.jp/Alexa%E8%AA%8D%E5%AE%9A%E5%8F%96%E5%BE%97%E8%A3%BD%E5%93%81%E3%80%91-%E7%9B%B4%E5%B7%AE%E3%81%97%E3%82%B3%E3%83%B3%E3%82%BB%E3%83%B3%E3%83%88-Google%E3%83%9B%E3%83%BC%E3%83%A0%E5%AF%BE%E5%BF%9C-%E9%9F%B3%E5%A3%B0%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%AB-HS105/dp/B078HSBNMT/ref=sr_1_2?ie=UTF8&qid=1544352973&sr=8-2&keywords=%E3%82%B9%E3%83%9E%E3%83%BC%E3%83%88+%E9%9B%BB%E6%BA%90)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/52947/3fd30e96-007c-8bad-e47c-bc6d1f605770.png)

・・・・・・あっ、会社の入り口はNFCカードで、ひとりひとりにカードが支給されている！
ということで、作成開始

## ちなみに

- エラー処理などは適当なところがあるのでご了承を。。。
- ぶっちゃけRaspberry Piから直接、ホワイトボードのWeb APIたたけるのですが・・・そこは考えないでくださいｗ
- （AWS IoT使ったりしてそのあたりの勉強とか、知見も得たかったので。。。）

## 構成

<img width="843" alt="スクリーンショット 2018-12-10 10.28.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/52947/9a8180e2-559b-7e22-7210-957a09a915be.png">


- タッチするとAWS IoT経由でLambdaを実行しホワイトボード更新、slackへ状態を通知
- CloudWatchのルールのスケジュール式を使用して、Lambdaを実行して、Shadow経由でRaspberry Piの状態（起動／シャットダウンなど）を制御、結果をslackへ通知
- ShadowのUpdate結果をslackへ通知
- TP-Link WiFiスマートプラグ経由でRaspberry Piに電源供給し、スマートプラグのスケジュール機能を使用して電源ON/OFFを行う


## 実装

### タッチ部分

- Python2系を使用
- ライブラリは[nfcpy](http://nfcpy.org/)を使用
- 主な処理
    - タッチしたときのIDmを取得、DBへ保存
    - 取得したIDmをSense HATのLEDに表示

nfcpyの情報はたくさんあり、すごく参考になりました！ありがとうございます。
SenseHATもPythonライブラリが提供されていて、超簡単にLEDにメッセージが表示できる！
でも、Python2系のみの対応みたいですね。。。
ソースはこんな感じ。

```python:main2.py
# -*- coding: utf-8 -*-

import nfc
from sense_hat import SenseHat

from models import SqliteDbManager, TouchHistoryModel


class MyCardReader(object):

    idm = None

    def on_connect(self, tag):
        print("touched")
        print(tag)
        self.idm = tag.identifier.encode("hex").upper()
        print(self.idm)
        return True

    def read_id(self):
        clf = nfc.ContactlessFrontend('usb')
        try:
            clf.connect(rdwr={'on-connect': self.on_connect})
        finally:
            clf.close()


def main():

    sense = SenseHat()
    cr = MyCardReader()

    while True:
        print("touch card:")

        cr.read_id()
        print("released")
        print(cr.idm)

        db_manager = SqliteDbManager()
        with db_manager.transaction() as session_pq:

            entry = TouchHistoryModel(idm=cr.idm)
            session_pq.add(entry)

        sense.clear()
        sense.show_message(cr.idm, text_colour=[0, 0, 100])

        cr.idm = None


if __name__ == '__main__':

    main()
```

そして、IDmをテーブルに保存するようにしました

#### なぜ保存するようにしたか

- 基本的にはPython3で実装したい
- でもnfcpyは2系のみ
- それなら、タッチ部分は「2系」、それ以外は「3系」で実装しよう
- DBでやりとりしよう！

ということにしましたｗ
DBはSqlite、ORMは[SQLAlchemy](https://www.sqlalchemy.org/)を使用

### 送信部分（タッチ部分以外）

- Python3系を使用
- AWS IoTへの送信は「[AWS IoT Device SDK for Python](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/iot-sdks.html#iot-python-sdk)」を使用
- 主な処理
    - ShadowのGet、Update処理
    - タッチ情報をDBから取得、AWS IoTへPublish
    - Subscribeして勤怠の変更後の状態を取得、表示
    - Shadowの値により、Raspberry Piの制御を行う

```python:main3.py
def main(loop):

    sense = SenseHat()

    touch_attendance_shadow = TouchAttendanceShadowData('{}')
    raspberry_pi_shadow = RaspberryPiShadowData('{}')

    count_sec = 0
    while True:

        # AWS IoT Shadow Get, Update
        if is_get_shadow(count_sec):
            # Shadowとの通信開始
            comm_aws_iot_shadow = CommAwsIoTShadow(thing_name)
            result = comm_aws_iot_shadow.connect()
            comm_aws_iot_shadow.create_shadow_handler_with_name(thing_name)

            # Shadowを取得
            shadow_payload = shadow_get(comm_aws_iot_shadow, loop)

            touch_attendance_shadow = TouchAttendanceShadowData(shadow_payload)
            raspberry_pi_shadow = RaspberryPiShadowData(shadow_payload)

            # Shadowを更新（deltaがある場合のみ）
            shadow_buf = BaseShadowData(shadow_payload)
            update_payload = shadow_buf.get_update_payload()
            shadow_update(comm_aws_iot_shadow, update_payload, loop)

            comm_aws_iot_shadow.disconnect()

        db_manager = SqliteDbManager()
        with db_manager.transaction() as session_pq:

            if touch_attendance_shadow.status:
                # AWS IoTへ送信
                touch_history_model = TouchHistoryModel()
                touch_history_entries = touch_history_model.select_all(session_pq)
                ok_ids = sub_pub_touch_histories(touch_history_entries, sense)
                touch_history_model.delete_by_id_list(session_pq, ok_ids)

        if raspberry_pi_shadow.is_reboot(count_sec):
            reboot(sense)

        if raspberry_pi_shadow.is_shutdown():
            shutdown(sense)

        time.sleep(1)
        count_sec += 1
```

#### ShadowのGet、Update処理

定期的に、ShadowのGetを行い、情報の取得とdeltaが存在する場合は、Updateを行う（5分間隔）

#### タッチ情報をDBから取得、AWS IoTへPublish、Subscribe

- 定期的にDBをselectして、タッチした情報が存在する場合、AWS IoTへPublish
- Publishが成功した場合は、DBのレコードを削除

#### Subscribeして勤怠の変更後の状態を取得、表示

- 更新後の状態をLambdaでpublishしているので、Raspberry Pi側でSubscribe
- 変更後の状態をSense HATのLEDに表示
- ちなみに失敗するときがあるので今後の課題・・・

#### Shadowの値により、Raspberry Piの制御を行う

設定値などは、Shadowにもたせて、Raspberry Pi側はその値をみて状態を制御するようにしました
Shadowの内容はこんな感じ

```json
{
  "desired": {
    "raspberry_pi": {
      "status": false,
      "reboot_interval_hour": 6
    }
  },
  "reported": {
    "raspberry_pi": {
      "status": false,
      "reboot_interval_hour": 6
    }
  }
}
```

- 起動後の累計時間が、再起動間隔（reboot_interval_hour）（単位：時）を超えた場合は再起動
    - 再起動処理をします表示をSense HATに表示
    - sudo shutdown -r now
- statusが「false」になった場合は、Raspberry Piをシャットダウン
    - シャットダウンします表示をSense HATに表示
    - sudo shutdown -h now

### AWS

- [serverless framework](https://serverless.com/framework/)を使用して構築
- 主な機能
    - Raspberry PiからPublishされたデータはAWS IoTのルールを使用してLambdaを実行し、ホワイトボードの変更などを行う
    - CloudWatchのルールのスケジュール式を使用して定期的にLambdaを実行しShadowをUpdate
    - ShadowのUpdate後にAWS IoTのルールを使用してLambdaを実行し結果をslackに通知

#### Raspberry PiからPublishされたデータはAWS IoTのルールを使用してLambdaを実行し、ホワイトボードの変更などを行う

こんなかたちで、ルールを登録

```YAML:serverless.yml
functions:
  update_attendance:
    handler: handler.update_attendance
    role: LambdaAttendanceRole
    events:
      - iot:
          name: "UpdateAttendance"
          sql: "SELECT *, topic(2) AS name FROM 'touch/+/histories'"
          description: "update white borad"
```

- Lambda内の処理（Python3）
    - IDmとユーザ情報を管理するテーブルからユーザ情報を取得
        - ない場合は、そのIDmの登録を行う
        - slackにその旨通知されるので、AWSコンソールから手動でユーザ情報を登録するｗ
    - ※以降はIDmに紐づくユーザ情報がある場合
    - 取得したユーザ情報をもとに、ホワイトボードから現在の状態を取得
        - Pythonの[requests](https://requests-docs-ja.readthedocs.io/)ライブラリを使用
    - 取得した状態を反転しホワイトボードを更新
    - 更新した状態をslackへ通知
        - Pythonの[slackweb](https://pypi.org/project/slackweb/)ライブラリを使用
    - 更新後の状態をPublish

slack通知例：

```python:handler.py
import ssl
from urllib.error import URLError, HTTPError
import slackweb
・
・
・
    def notify([webhookのURL], message):

        ssl._create_default_https_context = ssl._create_unverified_context
        slack = slackweb.Slack(url=[webhookのURL])

        try:
            slack.notify(text=message, username="attendance-bot", icon_emoji=":chinoppy:")
        except HTTPError as e:
            [エラー処理]
        except URLError as e:
            [エラー処理]
```

Publish 例：

```python:handler.py
# AWS IoTエンドポイント
endpoint = '[エンドポイント]'
# IoTDataPlaneクライアント
iot_data = boto3.client('iot-data', endpoint_url=endpoint)
・
・
・
    response = iot_data.publish(
        topic='touch/' + thing_name + '/histories/res',
        qos=1,
        payload=json.dumps({
            "更新後の状態": 更新後の状態（出勤／退社）,
        })
    )
```

#### CloudWatchのルールのスケジュール式を使用して定期的にLambdaを実行しShadowをUpdate

今のところは、Raspberry Piを

- 6時（JST）に起動指示
- 22時（JSL）にシャットダウン指示

```YAML:serverless.yml
functions:
  start_attendance:
    handler: handler.start_attendance
    description: start attendance
    role: LambdaAttendanceRole
    events:
      - schedule: cron(0 21 ? * * *)
  stop_attendance:
    handler: handler.stop_attendance
    description: stop attendance
    role: LambdaAttendanceRole
    events:
      - schedule: cron(0 13 ? * * *)
```

ShadowのUpdate例：

```python:handler.py
def _shadow_update(thing_name, update_payload):

    # Thing Shadowの有無を取得
    is_shadow = _is_shadow(thing_name)
    # shadowが存在する場合、updateする
    if not is_shadow:
        return

    iot_data.update_thing_shadow(
        thingName=thing_name,
        payload=update_payload
    )

def _is_shadow(thing_name):
    try:
        response = iot_data.get_thing_shadow(
            thingName=thing_name
        )
        payload = response['payload']
    except Exception as e:
        print(e.args)
        return False

    return True
```

#### ShadowのUpdate後にAWS IoTのルールを使用してLambdaを実行し結果をslackに通知

ShadowがUpdateされたかを、[予約されたMQTTトピック](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/device-shadow-mqtt.html)を使用して確認

```YAML:serverless.yml
functions:
  notification_shadow_update_accepted:
    handler: handler.notification_shadow_update_accepted
    role: LambdaAttendanceRole
    events:
      - iot:
          name: "NotificationShadowUpdateAccepted"
          sql: "SELECT *, topic(3)  AS name FROM '$aws/things/+/shadow/update/accepted'"
          description: "shadow update accepted"
  notification_shadow_update_rejected:
    handler: handler.notification_shadow_update_rejected
    role: LambdaAttendanceRole
    events:
      - iot:
          name: "NotificationShadowUpdateRejected"
          sql: "SELECT *, topic(3) AS name FROM '$aws/things/+/shadow/update/rejected'"
          description: "shadow update rejected"
  notification_shadow_get_rejected:
    handler: handler.notification_shadow_get_rejected
    role: LambdaAttendanceRole
    events:
      - iot:
          name: "NotificationShadowGetRejected"
          sql: "SELECT *, topic(3) AS name FROM '$aws/things/+/shadow/get/rejected'"
          description: "shadow get rejected"
```

### TP-Link WiFiスマートプラグ

- Wifiに接続しておけばどこからでも、アプリで電源のON/OFFができる
- スケジュール機能がある

Raspberry Pi側は

- 6時に起動指示
- 22時にシャットダウン指示

なので、TP-Link WiFiスマートプラグ側を

- 7時に電源ON
- 22時10分に電源OFF（Shadowの状態を5分間隔でチェックしているので余裕をもたせて）
- 電源のON/OFFは平日のみ

にするように設定

<img width="320" alt="IMG_2668.png" src="https://qiita-image-store.s3.amazonaws.com/0/52947/9f689a11-d5ed-624b-099a-f3ae88ea9fea.png">

これで、すべての実装、設定が完了！と思ったのですが、Systemdで自動起動するようにしておきます

```bash:start2.sh
#!/bin/bash

source /home/pi/touch_attendance/env2/bin/activate
cd /home/pi/touch_attendance
sudo python /home/pi/touch_attendance/main2.py
```

```:touch_attendance2.service（タッチ部分）
[Unit]
Description=touch attendance Python2

[Service]
Type=simple
WorkingDirectory=/home/pi/touch_attendance/
ExecStart=/home/pi/touch_attendance/start2.sh
Restart=always

[Install]
WantedBy = multi-user.target
```

```bash:start3.sh
#!/bin/bash

source /home/pi/touch_attendance/env3/bin/activate
cd /home/pi/touch_attendance
python /home/pi/touch_attendance/main3.py
```

```:touch_attendance3.service（タッチ部分以外）
[Unit]
Description=touch attendance Python3

[Service]
Type=simple
WorkingDirectory=/home/pi/touch_attendance/
ExecStart=/home/pi/touch_attendance/start3.sh
Restart=always

[Install]
WantedBy = multi-user.target
```

```bash
# シンボリックリンクをはる
$ sudo ln -s /home/pi/touch_attendance/touch_attendance2.service /etc/systemd/system/touch_attendance2.service
$ sudo ln -s /home/pi/touch_attendance/touch_attendance3.service /etc/systemd/system/touch_attendance3.service
# 自動起動設定
$ sudo systemctl enable touch_attendance2
$ sudo systemctl enable touch_attendance3
```

準備完了！

## さぁ試してみよう！

### タッチ！

#### PaSoRiにカードをタッチするとLEDにIDmと、変更後の状態が表示される

【出社だよー！】

![ezgif.com-video-to-gif (1).gif](https://qiita-image-store.s3.amazonaws.com/0/52947/00f5d3f6-7add-0e03-14be-9609aa6a0789.gif)

- slackに通知される

<img width="288" alt="スクリーンショット_2018-12-10_1_10_09.png" src="https://qiita-image-store.s3.amazonaws.com/0/52947/98865cec-b240-10fc-31ff-0e34108d360f.png">

【退社だよー！】

![ezgif.com-video-to-gif (2).gif](https://qiita-image-store.s3.amazonaws.com/0/52947/8dfb9b0a-f3af-36ed-d5f6-479c00f4d6d5.gif)

- slackに通知される

<img width="278" alt="スクリーンショット_2018-12-10_1_10_23.png" src="https://qiita-image-store.s3.amazonaws.com/0/52947/cd08a713-7375-a664-8274-fd4450de28ce.png">

### Shadowの状態がUpdateされたときにslackへ通知

#### Lambdaからの起動指示／シャットダウン指示がslackへ通知される

時間も想定どおり！
<img width="349" alt="スクリーンショット_2018-12-10_1_17_19.png" src="https://qiita-image-store.s3.amazonaws.com/0/52947/075b9bcb-3c59-af70-a6be-7ea2bb391758.png">

#### Raspberry PiがUpdateしたときにslackへ通知

「・・・起動します」〜「・・・シャットダウンします」までは、ホワイトボードの変更が可能
これでRaspberry Piの状態（起動中or停止中）がわかる！

<img width="269" alt="スクリーンショット_2018-12-10_1_19_20.png" src="https://qiita-image-store.s3.amazonaws.com/0/52947/7b969b79-75e5-8192-a146-7a52939e0a35.png">
<img width="288" alt="スクリーンショット_2018-12-10_1_10_09.png" src="https://qiita-image-store.s3.amazonaws.com/0/52947/98865cec-b240-10fc-31ff-0e34108d360f.png">
・・・
<img width="278" alt="スクリーンショット_2018-12-10_1_10_23.png" src="https://qiita-image-store.s3.amazonaws.com/0/52947/cd08a713-7375-a664-8274-fd4450de28ce.png">
<img width="355" alt="スクリーンショット_2018-12-10_1_19_37.png" src="https://qiita-image-store.s3.amazonaws.com/0/52947/e1ba83f6-9982-1693-b818-fc97c4a8ea35.png">

## 想定外だったこと

### IDmがタッチするたびにかわった？！

後輩ちゃん：「chinoppyさん、スマフォでもできるんですよね？」
chinoppy：「オフコース！」

後輩ちゃんがPaSoRiにタッチしたので、
DynamoDBのIDmとユーザ情報を保持しているテーブルにユーザ情報を登録

chinoppy：「これでできるよー」
後輩ちゃん：「最高ですね！」

もう一度、後輩ちゃんがタッチ

chinoppy：・・・「あれ？ホワイトボードかわらないなぁ・・・」

DynamoDBのIDmとユーザ情報を保持しているテーブルをみると
また別のIDmが登録されている・・・

もう一度、後輩ちゃんにタッチしてもらうと、また別のIDmが登録された・・・
今後の課題となりました・・・orz

## 最後に

今後は、勤怠もWeb化してあるので、タッチで時間がはいるようにしたい！
それでは、シャットダウンしますー

![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.amazonaws.com/0/52947/c8cfc31d-00a0-1c28-8226-8fa9ef342b20.gif)

明日は、[\_rio\_](https://qiita.com/_rio_)さんです！