1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

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

解決したいこと

Remo miniの赤点滅(画像)。こうなると要求を受け付けない。頻繁には起きないが、たまに点滅しているので困る・・・。(再起動で復活するので、この点滅を検知して再起動させる仕組みを作る)
IMG_0591.jpg

全体構成

image.png

自宅⇒AWS

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

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 IMG_0588.jpg

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

学びと対応

今回作ったものたちを次回触るのは、多分だいぶ未来になると思う(メンテ?機器追加?)。次回忘れていても、理解しやすくするために、API周りはswagger.yamlに、IPアドレスなどconfigパラメータはsetting.jsonにまとめた。画像中の.shファイルはsystemd serviceに登録するもの。実は1年ほど前に、一回模擬版を作ってみたことがあったのだが、1年経って見直してみたらハードコードでパラメータを含んだ形でコードが書かれていて、大分汚くて読み解くのに苦労した。拡張性考えてシンプルに作っておくのが一番大事と認識。
image.png

作ったもの

死活監視部分

グローバルIPアドレス、各デバイスの状態を死活状態を取得。

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ファイル

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

IoT Core受信部分

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

AWSのSDKサンプル

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のトリガとして使えることが分かった。このあたりうまく組み合わせてほかにも色々自動化していきたい。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?