いっぺーちゃんの いろいろやってみよ~

micropython on ESP32 でIFTTT(アクション編)

ジャッキー・チェンがアチョーって、、、またスベった。。。

今回はIFTTTのwebhooksチャネルをアクション(that)で利用し、ESP32側で処理を行うことを試してみます。
ESP32は家庭内LANなどに接続されており、外部から直接アクセスできないので、こちらを参考にESP32とのデータを直接やりとりするのにBeebotteを使用することにします。

データの流れは以下のような感じです。

    IFTTT --REST API--> beebotte --MQTT--> ESP32

まずbeebotteにユーザ登録します。
Beebotteにアクセスし、右上の「Sign up」から必要事項を入力し、「SIGN UP」をクリックすると、登録したe-mailアドレス宛に確認メールが届きます。
このメールに記載されているリンクをクリックすると登録完了です。

 

チャネルとリソースの作成、動作確認はいつものように先人の知恵を拝借。
IFTTTのトリガーおよびアクションをESP8266で実行する - Qiita

beebotteへのアクセス用URLには上記ページにある、
https://api.beebotte.com/v1/data/write/《チャネル》/《リソース》?token=《チャネルトークン》
のほか、
https://api.beebotte.com/v1/data/publish/《チャネル》/《リソース》?token=《チャネルトークン》
も使用できます。

これらの違いは、writeがストレージにデータを記録するのと同時にメッセージ配信を行うのに対し、publishはその場でメッセージを配信するだけだという点です。
その場でメッセージ配信するだけで良いならば、publishで良いでしょう。
ただし、Send on Subscribe(SoS)を使用するにはwriteでなければなりません。

IFTTT側の設定も上記ページを参考にしてください。
注意事項として、以下があります。

  • 送信するデータはダブルクォーテーションで囲む(例:「"送信データ"」)
    • 特に、Add ingredientで項目を選択したときに忘れがちです。
  • 送信するデータに改行が含まれないようにする。含まれているとIFTTTのレシピがエラーになり、データが送信されません。
    • たとえば、トリガをTwitterにした場合、送信データにtweet本文(Text)を指定すると、tweet本文に改行が含まれているとエラーになります。
    • 以下のような回避策が考えられます。
      • 本文に改行を入れないように運用する
      • 本文を送るのを諦め、特定のハッシュタグが含まれていたら固定文字列やなどを送るようにする
        • たとえば、ハッシュタグ#LIGHTが含まれていたらtweet本文を送信、ON/OFFをESP32側で判別する、という方法ではなく、ハッシュタグ#LIGHT_ON が含まれていたらONを送る、#LIGHT_OFFが含まれていたらOFFを送る、といった具合にIFTTTのレシピで分けておく。

beebotteとESP32間の通信はMQTTを使用します。
mqtt通信用モジュールはPypiにumqtt.simpleモジュールがあるので、それを利用します。
あらかじめupip(upipmではない)でumqtt.simpleモジュールをインストールします。

import upip
upip.install("micropython-umqtt.simple")

umqtt.simple についてはこちらを参照してください。
ソースはこちらで。

 

準備ができたら以下のプログラムを実行します。
チャネルトークン、チャネル名、リソース名に使用するデータを設定してください。
IFTTTからトリガがかかるたびにbeebotteにデータが送信され、最終的にcallback_func()が実行されます。

CHANNEL_TOKEN = '取得したチャネルトークン'

CHANNEL_NAME  = 'チャネル名'
RESOURCE_NAME = 'リソース名'

PING_PERIOD   = 120         # pingを打つ間隔(単位:秒)

from umqtt.simple import MQTTClient
import ujson
import utime

MQTT_SERVER = 'mqtt.beebotte.com'                     # サーバ名
MQTT_USER = 'token:' + CHANNEL_TOKEN                  # ユーザ名
MQTT_TOPIC = CHANNEL_NAME + '/' + RESOURCE_NAME       # トピック名

def callback_func(topic, msg):
    print("topic:%s  msg:%s" % (topic, msg))
    json_data= ujson.loads(msg)
    dt = json_data.get("data", "UNKNOWN")
    print("*** " + str(dt) + " ***")

# 初期化
c = MQTTClient("umqtt_client", MQTT_SERVER, user=MQTT_USER, password='', keepalive=PING_PERIOD*2)
# 接続
c.connect()
# コールバックの設定
c.set_callback(callback_func)
# サブスクライブ実行
c.subscribe(MQTT_TOPIC)

prompt = ['-', '\\', '|', '/']
cnt = 0
prevtime = utime.time()        # pingを打つための前回実行時刻を初期化
while  True:
    curtime = utime.time()
    if (curtime - prevtime) > PING_PERIOD :
        # 接続保持のためPING_PERIOD毎にpingを打つ
        c.ping()
        print('ping')
        prevtime = curtime       # 前回時刻の更新
    
    c.check_msg()                 # pingを打つ必要があるので、ノンブロッキング動作
    print(prompt[cnt], end="\r")  # 動作確認用表示
    cnt = (cnt + 1) & 0x03

c.disconnect()                    # 無限ループなのでここには来ない

以下解説です。

 

データを受信したときに実行されるコールバック関数です。
取得したデータのトピックとデータを表示します。
また、データはJSONデータ文字列なので、エンコードし、dataプロパティを取得して表示しています。
実際にはここで変数dtに応じて処理を行います(LEDを点灯/消灯する等)。
データの取得にはdataプロパティが存在しなかった場合に備え、getメソッドを使用しています。

def callback_func(topic, msg):
    print("topic:%s  msg:%s" % (topic, msg))
    json_data= ujson.loads(msg)
    dt = json_data.get("data", "UNKNOWN")
    print("*** " + str(dt) + " ***")

 

MQTTモジュールの初期化です。
ユーザ名には'token:'+チャネルトークン、パスワードには空文字列を指定します。
keepalive時間はPING_PERIOD(pingを打つ間隔)の2倍の値を設定しています。
MQTTブローカはkeepaliveに設定した時間の1.5倍の間接続を維持しますので、実際にはPING_PERIODの3倍の時間 接続が維持されます。
keepaliveを設定しない(or 0を設定)と、接続が切断されたことが通知されません(本来は切断されないのですが、beebotteでは5分くらいで接続断になってしまうようです。もしかしたらうちのネットワーク環境のせいかもしれませんが)。そこで、keepaliveを設定することで接続断時に例外がraiseされるようにしています。
(説明がわかりにくいですが、keepaliveの設定の有無の違いはwhileループ内のc.ping()を削除して10分ぐらい放置したあと、データを送信してみると違いが分かると思います)

また、この設定ではSSLを有効にしていません。SSLを有効にするにはパラメータに「ssl=True」を追加してください。
ただし、linux版micropythonはSSLのノンブロッキング動作に対応していないので、SSLを有効にするとエラー終了します。

c = MQTTClient("umqtt_client", MQTT_SERVER, user=MQTT_USER, password='', keepalive=PING_PERIOD*2)

 

接続~コールバックの設定~サブスクライブの実行の処理です。
トピック名は チャネル名/リソース名 で指定しますが、リソース名にワイルドカード(#)を指定することができるようです。
つまり、トピック名に チャネル名/# と指定すれば、チャネル内のすべてのトピックのデータが取得できるということです(使い道があるかはわかりませんが)。
トピック名に'#'と指定すると、すべてのチャネルのすべてのリソースのデータが取得できそうな気がしますが、beebotteではエラーになるようです。

c.connect()
c.set_callback(callback_func)
c.subscribe(MQTT_TOPIC)

 

pingを打つための時刻を取得するために現在の時刻を取得しておきます。
相対時間が必要なだけなので、時刻合わせを行う必要はありません。

prevtime = utime.time()        # pingを打つための前回実行時刻を初期化

 

データ取得のために無限ループにします。
接続断時やエラー発生時は例外がraiseされるので、try~except~ で囲んでおくのが良いかもしれません。

while  True:

 

keepaliveを設定してあるので、何もしないと接続が切断されてしまいます。それを防ぐために一定時間ごとにpingを打ちます。
他の処理の処理時間によってはPING_PERIODよりも長くなることがありますが、keepaliveがPING_PERIOD*2と余裕を持って設定されているので、大丈夫でしょう(たぶん、おそらく、もしかしたら。。。)

    curtime = utime.time()
    if (curtime - prevtime) > PING_PERIOD :
        # 接続保持のためPING_PERIOD毎にpingを打つ
        c.ping()
        print('ping')
        prevtime = curtime       # 前回時刻の更新

 

定期的に受信処理を呼び出します。
受信データがあったら、コールバック関数が実行されます。ここにデータが返ってくるわけではありません。

    c.check_msg()                 # pingを打つ必要があるので、ノンブロッキング動作

 

メインループで処理が必要な場合はここで処理します。

    print(prompt[cnt], end="\r")  # 動作確認用表示
    cnt = (cnt + 1) & 0x03