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

micropython on ESP32 でIFTTT(トリガ編)

micropythonでTwitterやLINE notifyにデータを送信するプログラムを作ってみましたが、IFTTTを使えば通知先を気にせず同じプログラムで送信できることに今更気が付きました。
ということで、IFTTTにデータを送信するプログラムを作ってみました。

まず、IFTTTにアクセスするモジュールをインストールします。
以下のプログラムをtiny_ifttt.pyという名前で保存し、いつものようにupipmでインストールしてください。

import usocket as socket
import ussl as ssl

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# ---- percent encoding
def RFC3986_encode(s):
    ret = ''
    bts = s.encode('utf-8')
    for c in bts :
        if c in range(0x30, 0x39 + 1) or \
           c in range(0x41, 0x5a + 1) or \
           c in range(0x61, 0x7a + 1) or \
           c in (0x2d, 0x2e, 0x5f, 0x7e):
            ret += chr(c)
        else :
            ret += '%%%02X' % (c)
    return ret

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class tiny_ifttt :
    def __init__(self, my_key, debug=False) :
        # パラメータチェック
        if type(my_key) is not str:
            raise ValueError("key must be string")
        
        self.my_key      = my_key
        self.__DEBUG__   = debug

    def __debug_print(self, str) :
        if self.__DEBUG__ :
            print(str)
    
    def __makeRequestMessage(self, host, event, value1, value2, value3) :
        # make request
        request = 'POST /trigger/' + event + '/with/key/' + self.my_key
        request += '?'
        if not value1 is None :
            request += 'value1=' + RFC3986_encode(value1) + '&'
        if not value2 is None :
            request += 'value2=' + RFC3986_encode(value2) + '&'
        if not value3 is None :
            request += 'value3=' + RFC3986_encode(value3) + '&'
        # 最後の「?」または「&」を削除して「 HTTP/1.1」をつける 
        request = request[0:-1] + ' HTTP/1.1\n'
        
        # make message header
        header  = 'Host: ' + host['host'] + '\n'                    \
                + 'User-Agent: micropython ifttt agent v0.1\n'      \
                + 'Connection: close\n'                             \
                + 'Accept: */*\n'

        # request message
        ret = request + header + '\n'
        return(ret)
    
    def __sendmessage(self, host, msg) :
        sock = socket.socket()
        addr = socket.getaddrinfo(host['host'], host['port'])[0][-1]
        
        # connect socket
        sock.connect(addr)
        try :
            # SSL wrap
            ssl_sock = ssl.wrap_socket(sock)
            
            # send data
            ssl_sock.write(msg)
            
            # 受信データの最初の1行
            rcv_line = ssl_sock.readline()
            protover, status, msg = rcv_line.split(None, 2)
            # self.__debug_print('%s::::%s::::%s' % (protover, status, msg))
            # status 200以外はエラー
            if status != b"200":
                ssl_sock.close()
                raise ValueError(status)
            # それ以外のレスポンスヘッダを読む
            while True:
                rcv_line = ssl_sock.readline()
                # self.__debug_print(rcv_line)
                if not rcv_line:
                    # なんらかの異常なレスポンス(ヘッダが終わる前にデータがなくなった)
                    ssl_sock.close()
                    raise ValueError("Unexpected EOF in HTTP headers")
                if rcv_line == b'\r\n':
                    # 空行でヘッダ終了
                    break
        except Exception as e:
            # エラーが発生したらクローズして上位へ例外通知
            ssl_sock.close()
            raise e
        # メッセージ本体を受信(とりあえず読み捨て)
        rcv_line = b''
        while True :
            try :
                l = ssl_sock.readline()
            except Exception as e:              # エラーが発生したらクローズして上位へ例外通知
                ssl_sock.close()
                raise e
           
            if not l:                           # データがない → 終了
                break
            
            # 読み込んだデータをためる
            rcv_line += l
        
        self.__debug_print("@@@@" + str(rcv_line))
        self.__debug_print("close!!")
        ssl_sock.close()
    
    # ######## notify API ################################
    def trigger(self, event, value1=None, value2=None, value3=None) : 
        host = { 'host': 'maker.ifttt.com', 'port': 443}
        
        reqMessage = self.__makeRequestMessage(host, event, value1, value2, value3)
        self.__debug_print('%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%')
        self.__debug_print(reqMessage)
        self.__debug_print('%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%')
        
        self.__sendmessage(host, reqMessage)

プログラム自体はLINEのときとそんなに変わりません。
大きく違うのは送信するデータの作成部分(__makeRequestMessage メソッド)だけです。

次に、IFTTTのレシピを作成します。
ESP32からのデータを受け取るにはwebhooksというチャネルを使用します。これはREST APIでデータを受け取るチャネルのようです。
旧称は MakerChannel のようで、ネット上にはこの名前で検索すると結構情報が出てきたりします。
トリガ(this)、アクション(that)それぞれに対応していますが、ここではトリガとして使用する方を試してみます。
具体的な設定手順は、例によって先人の知恵を拝借します。
Nefry BTとIFTTTでスイッチを押したらLINEを送る仕組みを作ってみよう | dotstudio
「最後にFinishを押し、IFTTTのレシピの作成は完了です。」までの部分です。

 

準備ができたら以下のプログラムを実行します。

my_keyにはアカウントごとに割り当てられるキーを設定します。
上記ページではsettingをクリックしてほにゃららと書いてありますが、settingの左のDocumentationをクリックすると一番上にデカデカと表示されます。
eventにはレシピに設定したイベント名を設定します。上記ページでは「Nefry」となっているものです。
triggerのパラメータ、value1, value2, value3 は必要なものだけ記述してください。不要ならすべて省略してもかまいません。(eventは必須)

my_key = '取得したキー'
event  = '作成したイベント'

from tiny_ifttt import tiny_ifttt

# 初期化
ti = tiny_ifttt(my_key, debug=True)
ti.trigger(event, value1='へろー', value2='えぶり', value3='わん')

成功すれば、コンソールに

@@@@b"Congratulations! You've fired the <<イベント名>> event"
close!!

と表示され、LINEに以下のようなメッセージが届きます。

[IFTTT] Value 1: へろー
Value 2: えぶり
Value 3: わん

届かない場合、IFTTTのトップページから上のほうにあるメニューの一番右の「Activity」で「Applet ran」という表示が出ているか確認してください。
(失敗している場合は「Applet failed」と表示されます)
まだ表示されていない場合は、メニューの右から2番目の「My Applets」で作成したレシピを選択、下の方にある「Check now」をクリックしてみてください。
(webhooksをトリガとするレシピは大体数秒で実行されるようですが、まれに数分以上かかることがあります)

 

LINEに届いた日本語文字列が%数字%数字~となっていた場合は、シリアルコンソールやファイルの文字コードUTF-8になっているか確認してください。
また、シリアルコンソールからプログラムを入力する場合は、ペーストモードでコピペしてください。通常モードだと日本語部分が消えます。

my_key の設定が間違っていた場合、例外が発生し、

ValueError: b'401'

と表示されます。

 

eventが間違っていた場合は、プログラムは正常に終了しますが、IFTTT側では何も起こりません。
(居ない人が呼ばれたんだから、誰も返事しないのは当然)

これで、ESP32側のプログラムは一つでLINEやTwitterFacebook、e-mailなど色々な通知先を選択(またはすべてに同時配信)することができます。

また、アクションにDropboxやGoogleDriveを使用すればセンサの測定データを定期的に保存するといった使用方法もできるでしょう。