Nature Remoの死活監視をraspberryPiとAWSで

# はじめに
Nature RemoはAlexaと連携出来たりAPIも公開されていたり非常に便利だが、いつの間にか接続が切れ、Alexaに話しかけても「○○は応答していません」と、操作が拒否されることがある。ホームIoTにおいて、家電がお願いを聞いてくれないのはユーザー体験が非常に悪い。お正月休みを使ってAWSとRaspberryPiで死活監視できるしくみを構築した。


### 解決したいこと
Remo miniの赤点滅（画像）。こうなると要求を受け付けない。頻繁には起きないが、たまに点滅しているので困る･･･。（再起動で復活するので、この点滅を検知して再起動させる仕組みを作る）
![IMG_0591.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54100/fa32fb83-9276-0633-d074-92a989b5c78b.jpeg)

## 全体構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54100/e5a31279-3eb1-485f-2a17-dd9c66f723de.png)


### 自宅⇒AWS
- raspberryPiから3分毎にRemoの死活をモニター
- Remoの死活はLocalAPI対応機種はLocalAPIでmessageを送信。
　LocalAPI非対応機種（NatureRemoNanoなど）はPingで監視する。
- モニターした結果をAPI Gateway経由でAWSに送信。　　

https://local-swagger.nature.global/

### AWS⇒自宅
- RaspiberryPiのすべての実行トリガはFastAPIに集約  
 （結果、スマホからのREST API操作にも対応できる）
- Remoに異常がみられる場合はIotCoreを通じてFastAPIをトリガする
- 一定時間内で何度も異常が繰り返される場合には本仕組みに何らかの障害が発生しているとし、LINEでユーザにアラート

### フェールセーフの対象
家庭内LAN環境なのですべては救えない･･･。  
本システムで対応するフェールセーフの対象は以下の通り。
| フェール | 対応 |
| ---- | ---- |
| 家庭内LANに障害 | Heartbeatが来ない、CloudWatchアラームで検知、LINE通知 |
| Tapo Plugに障害 | ローカルRaspiberryPiで検知、AWS側からLINE通知 |
| Remoが赤点滅 | 検知＆Plug再起動 ←**今回の対象！**|
| その他障害 | 異常時実行lambdaの実行回数が多い⇒CloudWatchアラームで検知 |


## Remo周りの物理構成
Tapoプラグ(Tapo P105)でON/OFFできるようにした。
|  | Nature Remo mini | Nature Remo nano |
| ---- | ---- | ---- |
| 死活 | LocalAPI | ping |
| 画像 | ![IMG_0590.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54100/9a0c8fab-a795-d09e-c5b4-6f96f62e981a.jpeg) | ![IMG_0588.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54100/a3dd4a2b-7c82-401d-f848-1bd57241070e.jpeg) |

PlugのON/OFF操作は有志がRESTで操作できるように、ライブラリを作ってくれている。これを活用する。超感謝。

https://gitlab.com/colinleroy/TapoPlug-Rest-API

## 学びと対応
今回作ったものたちを次回触るのは、多分だいぶ未来になると思う（メンテ？機器追加？）。次回忘れていても、理解しやすくするために、API周りは`swagger.yaml`に、IPアドレスなどconfigパラメータは`setting.json`にまとめた。画像中の`.sh`ファイルはsystemd serviceに登録するもの。実は1年ほど前に、一回模擬版を作ってみたことがあったのだが、1年経って見直してみたらハードコードでパラメータを含んだ形でコードが書かれていて、大分汚くて読み解くのに苦労した。拡張性考えてシンプルに作っておくのが一番大事と認識。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54100/ceac4db8-15a0-9a40-fe79-8ef3e63bcb69.png)

## 作ったもの
#### 死活監視部分
グローバルIPアドレス、各デバイスの状態を死活状態を取得。
```python:polling/main.py
import requests
from ping3 import ping, verbose_ping
import json


def get_gip_addr():
    res = requests.get("https://ifconfig.me",timeout=5)
    print("get_gip_addr", res.text)
    return res.text


def get_RemoStatus(devices_):
    code = 400
    for device in devices_:
        # natureRemoの高級機、API対応版
        if device["type"] == "localAPI":
            ipAddr = device["ipAddr"]

            headers = {
                "X-Requested-With": "local",
                "Content-Type": "application/x-www-form-urlencoded",
            }

            postData = '{"format":"us","freq":37,"data":[222]}'

            try:
                response = requests.post(
                    "http://" + ipAddr + "/messages",
                    headers=headers,
                    data=postData,
                    timeout=5,
                )
                code = response.status_code
            except:
                code = 400
                break
        # natureRemoの廉価版、localAPI非対応版
        elif device["type"] == "ping":
            ipAddr = device["ipAddr"]
            ping_res = ping(ipAddr)

            if ping_res > 0:
                code = 200
            else:
                code = 400
                break
    print("get_RemoStatus", code, device)
    return code

def get_PlugStatus(devices_):
    code = 400
    for device in devices_:
        ipAddr = device["plug_ipAddr"]
        ping_res = ping(ipAddr)

        if ping_res > 0:
            code = 200
        else:
            code = 400
            
            break
        print("get_PlugStatus", code, device)
    return code

def post(ipAdress, natureRemoStatusCode,tpLinkPlugStatusCode, urlEndpoint, APIkey, ipAddrListToReset):
    print(ipAdress, natureRemoStatusCode,tpLinkPlugStatusCode, urlEndpoint, APIkey, ipAddrListToReset)
    headers = {"x-api-key": APIkey}
    itemData = {
        "ipAdress": ipAdress,
        "natureRemoStatusCode": natureRemoStatusCode,
        "tpLinkPlugStatusCode": tpLinkPlugStatusCode,
        "ipAddrListToReset": ipAddrListToReset
        }
    rSuccess = requests.post(urlEndpoint, headers=headers, json=itemData)


if __name__ == "__main__":
    file_path = "setting.json"
    with open(file_path, "r") as file:
        jsonData = json.load(file)

    urlEndpoint = jsonData["aws"]["urlEndpoint"]
    APIkey = jsonData["aws"]["APIkey"]
    devices = jsonData["local"]["devices"]
    deviceIPAddr = [i["plug_ipAddr"] for i in jsonData["local"]["devices"]]
    post(get_gip_addr(), get_RemoStatus(devices),get_PlugStatus(devices), urlEndpoint, APIkey, deviceIPAddr)

```

configパラメータ設定用のjsonファイル
```json:poling/config.json
{
    "aws":{
        "ARN":"arn:aws:execute-api:ap-northeast-1:xxxxx:xxxxx/*/POST/",
        "urlEndpoint":"https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com",
        "APIkey": "xxxxxxxxx"
    },
    "local":{
        "devices":[
            {
                "type":"localAPI",
                "name":"natureRemoMini",
                "ipAddr":"192.168.50.x",
                "plug_ipAddr":"192.168.50.x"
            },
            {
                "type":"ping",
                "name":"natureRemoNano",
                "ipAddr":"192.168.50.x",
                "plug_ipAddr":"192.168.50.x"
            }
        ]
    }
}
```

APIの説明はswaggerにちゃんと書く。将来のため。コードから読み起こすのはつらい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54100/34ca80f4-856e-ffc2-1177-4c5befbe05d1.png)



#### IoT Core受信部分
AWSのSDKサンプルを少しだけ改造。
常にMQTTのSubscribe状態にしておいて、AWS側のメッセージを受け取れるようにしておく。メッセージが届くと、受け取ったjson中身を開いてクエリにし、FASTAPIのエンドポイントを叩く。

AWSのSDKサンプル

https://github.com/aws/aws-iot-device-sdk-python-v2/blob/main/samples/pubsub.py

```diff_python
def on_message_received(topic, payload, dup, qos, retain, **kwargs):
    print("Received message from topic '{}': {}".format(topic, payload))
    global received_count
    received_count += 1
    if received_count == cmdData.input_count:
        received_all_event.set()

+    query = payload.decode('utf-8')
+    jsonData = json.loads(query)
+   if jsonData["type"] == "reboot":
+       requests.get("http://localhost:8000/reboot/?ipAddr="+jsonData["ipAddr"], timeout=(3.0, 7.5))
```

# 最後に
いろんな機能のトリガをLAN内のRaspiberryPiのFastAPIに集約しておくと、スマホで操作ができ、やれることの幅が広がるように思う。実際、NFCタグとスマホで一括OFF操作などもできるようにしている。一方で、LAN内だけのエンドポイントだと屋外からの操作に支障がでる。その場合、IotCoreがFastAPIのトリガとして使えることが分かった。このあたりうまく組み合わせてほかにも色々自動化していきたい。